第二章 C++ 数据类型、变量与复合结构
在 C++ 语言中,数据类型是程序处理信息的基础。合理地选择数据类型、科学地组织变量与数组,以及理解底层指针和引用机制,是编写高效算法程序的基石。本章将系统讲解 C++ 的基本数据类型、变量与常量的规则、数组与字符串、指针与引用的概念,以及结构体的应用。
2.1 基本数据类型
C++ 提供了多种基本数据类型以满足不同的存储和计算需求。下表列出了常用的基本数据类型及其占用空间和数据范围:
| 名称 | 占用空间(字节) | 常用别名 | 数据范围 |
|---|---|---|---|
int |
4 | signed, signed int, long, long int |
$-2,147,483,648 \sim 2,147,483,647$ |
unsigned int |
4 | unsigned, unsigned long |
$0 \sim 4,294,967,295$ |
char |
1 | char |
$-128 \sim 127$ |
unsigned char |
1 | unsigned char |
$0 \sim 255$ |
short |
2 | short int, signed short |
$-32,768 \sim 32,767$ |
unsigned short |
2 | unsigned short int |
$0 \sim 65,535$ |
long long |
8 | signed long long |
$-9,223,372,036,854,775,808 \sim 9,223,372,036,854,775,807$ |
unsigned long long |
8 | 无 | $0 \sim 18,446,744,073,709,551,615$ |
bool |
1 | 无 | true 或 false |
float |
4 | 无 | $3.4\text{E}\pm38$(7位有效数字) |
double |
8 | 无 | $1.7\text{E}\pm308$(15位有效数字) |
C++11 新增:固定宽度整数类型
为了保证程序在不同平台、不同编译器下具有完全一致的位宽行为,C++11 在头文件 <cstdint> 中引入了固定宽度的整数类型:
| 类型 | 含义 | 范围 |
|---|---|---|
int8_t / uint8_t |
8位有符号 / 无符号整数 | $-128 \sim 127$ / $0 \sim 255$ |
int16_t / uint16_t |
16位有符号 / 无符号整数 | $-32,768 \sim 32,767$ / $0 \sim 65,535$ |
int32_t / uint32_t |
32位有符号 / 无符号整数 | 同 int / unsigned int |
int64_t / uint64_t |
64位有符号 / 无符号整数 | 同 long long / unsigned long long |
竞赛建议:在算法竞赛中,推荐使用
int64_t替代传统的long long。它的可读性更强,且能够从语言标准层面保证在任何平台下都是精确的 64 位宽,避免因平台差异导致意料之外的溢出错误。
2.2 变量与常量
1. 定义变量
- 语法格式:
变量类型 标识符;(例如:int i;)。 - 未初始化警告:局部变量在定义时如果未进行显式初始化,其在内存中的初始值是不确定的(通常是残留的脏数据)。因此在声明变量后,建议立即初始化,如
int i = 0;。
2. 定义常量
- 语法格式:
const 变量类型 标识符 = 初始值;(例如:const int N = 90;)。常数的值在定义后不能被修改。
3. 合法的标识符规则
- 标识符不能与 C++ 的关键字(如
int,double,const等在集成开发环境中通常会变色的词语)冲突。 - 标识符只能包含字母(大写和小写)、数字和下划线
_,且开头只能是字母或下划线,不能以数字开头。 - 标识符必须先定义后使用。
- 在同一作用域内,标识符不能重复定义。为了防止逻辑混淆,在不同作用域内也应尽量避免定义同名变量。
- C++ 严格区分大小写,例如
Variable和variable是两个完全不同的标识符。
4. 字面量后缀(C++11)与数字分隔符(C++14)
- 整数字面量后缀:对于超出整型范围的字面量,应显式附加后缀。例如
123LL(代表long long类型字面量),123ULL(代表unsigned long long类型字面量)。 - 二进制字面量(C++14):允许直接以
0b或0B开头书写二进制常数,如0b1010等于十进制的10。 - 数字分隔符(C++14):允许在长数字字面量中使用单引号
'作为分隔符以提高代码可读性,编译器在编译时会自动忽略单引号。cpp long long x = 123'456'789'012LL; // 极大地提高了长数字的可读性 int y = 0b1010'1010; // 二进制表示 10101010,等于十进制 170
2.3 数组与字符串
1. 一维数组
- 定义:
int a[10];。该数组在内存中占用了 10 个连续的int空间,下标范围为0 ~ 9。访问第 6 个元素时使用a[5]。
2. 二维数组
- 定义:
int b[5][3];。该数组共有 $5 \times 3 = 15$ 个元素,在内存中按行连续存放。元素依次为b[0][0]、b[0][1]、b[0][2]、b[1][0]$\dots$ 直到b[4][2]。访问具体元素时需要使用两个方括号,如b[2][1]。更长维度的多维数组定义与此类推。
3. 数组名与元素寻址
- 数组名本质上是一个常量指针,指向该数组首个元素的内存地址。例如
a代表&a[0],b代表&b[0][0]。 - 多维数组的本质是“数组的数组”。因此,二维数组
b的第一维b[0]实际上是包含 3 个一维元素的子数组的数组名,即b[0]相当于&b[0][0]。 - 数组在内存中是连续排布的。因此可以利用指针偏移量进行寻址运算:
a + 3对应&a[3],*(b + 1)对应b[1][0],*(*(b + 3) + 2)对应b[3][2]。竞赛建议:在算法竞赛中,应尽量避免复杂的指针算法寻址,直接使用下标(如
b[3][2])可以使代码更清晰,降低调试难度。
4. 字符串
- 在 C 风格语法中,字符串在物理上表现为
char类型的数组。 - 零终止符
\0:字符串的最后一个字符必须是空字符'\0'(ASCII 值为 0)。如果缺少\0,在使用标准输出或字符串处理函数时,程序会因为无法定位边界而发生内存越界和未定义行为。 - 整体操作限制:原生数组(包括
char字符数组)不能直接进行整体赋值或比较(例如不能写a = b或if (a == b))。若需要进行这些操作,应使用<cstring>中的memcpy和memcmp库函数。如果是字符串,则应使用strcpy和strcmp。
数组越界警告: * C++ 数组下标必须从 0 开始。
int a[10]的最大合法索引是a[9],不存在a[10]。 * C++ 在运行期不进行任何数组越界检查。如果发生数组越界访问,可能会静默修改相邻变量的值,或导致程序发生不可预知的崩溃。
2.4 指针
指针是用于保存另一个变量内存地址的特殊变量。
1. 基本操作符
- 取地址运算符
&:返回一个变量在内存中的起始地址。由于数组名本身在表达式中会自动转换为指向首元素的指针,因此对数组名无须使用&运算符。 - 指针运算符
*:作用于指针变量,用于获取或修改该地址所指向的内存空间中的实际值。
2. 定义与初始化
- 定义语法:
int *p;。在同一行声明多个指针时,每个变量名前都必须加上*标识(例如int *p1, *p2;)。 - 野指针警告:如果指针
p在定义后没有被显式初始化,它会指向一个随机的未知内存空间。对这种“野指针”进行解引用写入操作,极易引发程序崩溃。在不用时应将其初始化为nullptr(C++11 标准)或NULL。
指针操作实例:
int a = 0, b = 1;
int c[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p; // 定义一个整型指针变量
p = &a; // 让 p 指向变量 a 的地址
(*p) = 3; // 相当于对 a 赋值,a = 3
(*p) = b; // 相当于 a = b,此时 a 的值变为 1
// p = b; // 非法:不能把 int 类型的值直接赋给指针变量 int*
p = &b; // 让 p 重新指向 b 的地址,此后 p 与 a 失去关联
p = c + 6; // 指针偏移,使 p 指向数组元素 c[6]
cout << *p; // 输出 p 指向的变量值,即输出 c[6] 的值(7)
p++; // 指针向后移动一个整型单位(4字节),此时指向 c[7]
p = nullptr; // 将 p 置为空指针,不指向任何有效地址
// cout << *p; // 警告:对空指针进行解引用会导致程序崩溃!
2.5 引用
引用是已存在变量的别名,在语法上充当了原变量的代言人。
int &p = f[0][2]; // p 成为 f[0][2] 的别名
此后,无论是修改 p 的值还是修改 f[0][2] 的值,操作的都是同一个内存单元。
引用的优势与应用:
- 简化代码与提高可读性:对于在程序中需要频繁读写的多维数组元素(如
f[0][2]),使用引用可以避免冗长的下标书写。 - 函数参数传递:在函数形参中使用引用类型(如
void update(int &x)),可以在函数内部直接修改外部实参的值,而无须像指针那样进行繁琐的取地址与解引用操作。
引用与指针的区别:
- 初始化要求:引用在创建时必须被立即初始化,且必须绑定到一个合法的内存单元上;而指针可以先定义,后续再进行赋值。
- 绑定关系唯一性:引用一旦初始化完成,它与原变量的绑定关系就不可更改,对其重新赋值只是改变原变量的值;而指针在生命周期内可以随时指向不同的内存地址。
2.6 结构体
结构体(struct)用于将多个不同类型的数据项组合成一个有机的整体。
struct pack {
int value; // 成员变量:价值
int weight; // 成员变量:重量
};
- 定义变量:在 C++ 中定义结构体变量时,可直接写成
pack p;(无须像 C 语言那样强制补写关键字struct)。 - 访问成员:
- 通过结构体对象本身访问:使用成员访问符
.(例如p.value)。 - 通过结构体指针访问:使用指向符
->(例如(&p)->value)。
- 通过结构体对象本身访问:使用成员访问符
- 面向对象特性:在 C++ 中,结构体表现出与类(
class)几乎一致的特性。结构体内部可以拥有自己的构造函数、成员函数,并且可以进行运算符重载(这在自定义sort的比较规则时极其有用)。 - 递归定义限制:结构体内部不允许包含其自身类型的成员变量(因为这会导致编译器无法计算其空间大小),但允许包含指向其自身类型的指针成员(如
pack *next;,这在链表结构中非常经典)。
2.7 课后推荐练习题
为了加深对数据范围、数组、结构体以及引用指针等基础知识的理解,建议在平台中练习以下精选题目:
- 题目编号:59 —— 类型转换与溢出
- 考点提示:本题中计算阶乘的值会随着输入迅速膨胀。通过此题,您可以深刻理解
int与long long的数值范围限制,并练习如何避免算术溢出。
- 考点提示:本题中计算阶乘的值会随着输入迅速膨胀。通过此题,您可以深刻理解
- 题目编号:95 —— 交换数值
- 考点提示:编写一个交换函数。要求利用引用传递(
&)作为函数参数,从而实现在函数体内部修改外部变量的值,掌握引用的核心用法。
- 考点提示:编写一个交换函数。要求利用引用传递(
- 题目编号:136 —— 成绩排序
- 考点提示:本题需要存储每个学生的学号和多科成绩。非常适合练习利用
struct结构体打包多属性数据,并编写自定义排序函数。
- 考点提示:本题需要存储每个学生的学号和多科成绩。非常适合练习利用
- 题目编号:237 —— 寻找最小值
- 考点提示:基本的一维数组练习题。通过数组存储输入序列,并使用循环遍历整个数组来寻找最小值及其在数组中的下标。
- 题目编号:245 —— 二维数组模拟
- 考点提示:经典的二维数组生成与填充题。需要定义合适大小的二维数组,并通过嵌套循环按照“回”字形规律进行逻辑判断和填充。
- 题目编号:1536 —— 删除链表节点
- 考点提示:指针与链表操作。给定链表中某个节点的指针,要求在不遍历链表的情况下完成对该节点的删除,深刻体会指针在动态数据结构中的精妙应用。
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com