欢迎访问我的博客首页。
变量与基本类型
1. 变量与基本类型
1.1 声明、定义、初始化、赋值
声明:使得名字为程序所知。
定义:创建与名字关联的实体。
初始化:创建变量时赋予其一个初始值。
赋值:把对象的当前值擦除,而以一个新值来替代。
声明是告诉程序有一个变量,并没有为该变量分配内存;定义是为变量分配内存,关联变量名字与内存;初始化是向定义了的变量的内存中写入指定的值。
声明和定义的区别可以从下面创建类的过程理解。类的声明过程没有占用内存,定义过程才申请内存。
定义和初始化的区别可以从引用理解。定义引用时必须初始化,其中绑定对象的过程就是初始化。定义与初始化容易让人混淆,这是因为如果定义变量时没有指定初值,变量可能被默认初始化,这样的定义就包含了初始化。顺便指出,在函数体内部定义内置类型变量不包含初始化:
// 1.类的声明。
class Example {
public:
void fun();
int a = 0;
static int b;
};
// 2.类成员的定义。
void Example::fun() { cout << "This is fun." << endl; }
//int Example::a = 0; //非静态数据成员不能在其类的外部定义。
int Example::b = 1;
第 4、5、6 行声明了一个函数成员、一个普通数据成员、一个静态数据成员。第 5 行只是给了一个类内初始值,直到创建对象时 a 才被用类内初始值初始化。
第 9 行定义了成员函数。第 10 行试图定义非静态数据成员,这是不被允许的。第 11 行定义并初始化静态数据成员(必须在类的外部定义和初始化每个静态成员)。类成员的初始化详情在第四章。
1.2 基本数据类型
1. 基本类型占用内存的大小
bool | char | short | int | long | long long | float | double | long double |
---|---|---|---|---|---|---|---|---|
1 | 1 | 2 | 4 | 4 | 8 | 4 | 8 | 8 |
引用类型与被引用对象所占字节大小相等。内置裸指针类型所占字节都是 8(包括数组指针、函数指针、类指针等)。
2. 基本类型表示的范围
整数是以二进制形式存放的。比如 int 占 4 字节,所以它有 31 位表示二进制数,1 位表示符号,用
N
N
N 代表
2
31
2^{31}
231,则 int 的范围是
[
−
N
,
N
−
1
]
[-N,N-1]
[−N,N−1]。
浮点数是以 10 为底的指数形式存放的,所以同样是 4 字节,float 的范围比 int 大得多。
在 C++ 中包含 limits.h 可以获取变量的最值。有符号数和无符号数的最值都有两种表示方式,下表以最大值为例:
类型 | bool | char | short | int | long | long long | float | double | long double |
---|---|---|---|---|---|---|---|---|---|
最大 | - | - | SHRT_MAX | INT_MAX | LONG_MAX | LLONG_MAX | FLT_MAX | DBL_MAX | LDBL_MAX |
最大 | - | - | MAXSHORT | MAXINT | MAXLONG | MAXLONGLONG | - | - | - |
无符号最大 | - | - | USHRT_MAX | UINT_MAX | ULONG_MAX | ULLONG_MAX | - | - | - |
无符号最大 | - | - | MAXUINT16 | MAXUINT | MAXUINT32 | MAXUINT64 | - | - | - |
size_t 是 unsigned long long 类型,最大值是 MAXSIZE_T。
布尔类型是 1 字节的无符号整型,可以赋值从 0 到 255 的整数。布尔型变量或其它类型的变量的值为 0 代表 false,非 0 代表 true。
2. 复合类型
2.1 数组
1. 一维数组和二维数组做形参
// 一维数组
void fun(int (&a)[3]);
void fun(int* a);
// 二维数组
void fun(int (&a)[2][3]);
void fun(int (*a)[3]);
void fun(int *a, size_t col); // 调用:fun(*a, 3); *(a + col * i + j)等价于a[i][j]。
第 2、3 行是一维数组的引用、指针做参数;第 6、7 行是二维数组的引用、指针做参数。第 2、6 行的形参是引用,sizeof(形参)= 数组长度。
形参 int* a 和 int a[] 等价,形参 int (*a)[3] 和 int a[][3] 等价。
2. 数组长度
常量表达式包括字面值、非类型模板参数,在编译时就能确定其值。
普通数组的每一维度都要用常量表达式或常量指定。数组的引用做形参,第二维要指定。
一维动态数组的长度可以用变量指定。
template<int N>
void fun(int a, const int b, int(&c)[N]/*, constexpr int e*/) {
constexpr int d = 4;
// 参数类型不能声明为常量表达式类型。
// 1.普通数组。
// 只有 d 和 N 可以在创建普通数组时指定维度。
// 2.动态数组。
int* p1 = new int[a];
int* p2 = (int*)malloc(sizeof(int) * a);
// p3、p4是二维数组指针,二维数组指针类型必须指明第二维长度。
// new 的第二维必须是常量表达式。malloc申请的字节数可以用变量指明。
int(*p3)[d] = new int[a][d];
int(*p4)[N] = (int(*)[N])malloc(sizeof(int) * a * N);
delete[] p1;
free(p2);
delete[] p3;
free(p4);
}
2.2 枚举
2.3 指针
指针的作用:1. 引用动态内存。2. 作为数组和容器等的元素。引用不是对象,不能存储在数组和容器中。
1. 普通指针的初始化
int a = 1;
int& r = a;
// 1.用变量初始化指针。
int* p1 = &a;
// 2.用指针和引用初始化指针。
int* p2 = p1;
int* p3 = &r;
2. 常量指针与指向常量的指针
int a = 1;
const int b = 2;
int* const p1 = &a; // 常量指针:指针是常量,定义时必须初始化且不可再指向其它变量。
const int* p2 = &a; // 指向常量的指针:不能通过指针修改所指变量的值。
int* const p3 = &b; // 错误:指向常量需要用指向常量的指针!
3. 一维数组指针、二维数组指针、函数指针
// 1.指向一维数组的指针。pa是指针,*(pa+i)等价于a[i]。
int a[5] = { 4, 3, 2, 1, 0 };
int* pa = a;
// 2.指向二维数组的指针。pb是指针,*(pb+i)等价于b[i]。
int b[2][3] = { {1,2,3},{4,5,6} };
int(*pb)[3] = b;
// 3.函数指针。
float (*pc1)(int, long) = fun; //定义一个函数指针pc1,指向float fun(int, long)这样的函数。fun加不加&都一样。
// 使用typedef。
typedef float (*Pc)(int, long);
Pc pc2 = fun;
Pc fArray[] = {f1, f2, f3}; //函数指针数组。
// 调用。pc1等价于pc2。
(*pc1)(1, 2);
pc1(1, 2);
// 4.函数指针做参数。三种方式等价。testPc是参数为函数(指针),返回值为void的函数。
void testPc(float p(int, long));
void testPc(float (*p)(int, long));
void testPc(Pc p);
// 5.函数指针做返回值。两种方式等价。函数testPc没有参数,它的返回值是一个函数指针,指向float fun(int, long)这样的函数。
Pc testPc();
float (*testPc())(int, long); //用法:float (*pc1)(int, long) = testPc();
4. 一维数组指针与数组名
数组名虽然可以当指针用,但它不等价于数组指针:
- 数组名是常量。
- sizeof 结果不同。sizeof(数组名) = sizeof(数组元素类型) × \times × 元素个数,而sizeof(数组指针) = 8。注意:sizeof(*二维数组指针) = sizeof(数组元素类型) × \times × 第二维的元素个数。
5. 指针越界和野指针
避免指针越界:
- 避免数组越界。
- 避免向一块内存中写过多内容。
- 避免多次释放同一指针。
- 避免使用空指针、野指针。
野指针:
5. 指针未被初始化。
6. 使用 delete 运算符或 free 函数释放后没有把指针置空。
7. 指针操作超出变量作用域。
指针定义后应该马上初始化,不再指向对象时应该马上置空。如果定义指针时没有初始化为空,然后使用 strncpy 这样的函数,会向未知内存写入。
2.4 引用
引用的作用:
- 形参或返回值使用引用类型可以避免拷贝对象。但不能返回局部变量的引用,详情在第三章 函数。
1. 普通引用的初始化
int a = 1;
int* p = &a;
// 1.用变量初始化引用。
int& r1 = a;
// 2.用指针和引用初始化引用。
int& r2 = *p;
int& r3 = r1;
// 3.指针的引用。从右往左阅读定义,离变量名近的符号对变量的类型有最直接的影响。
int*& r4 = p;
2. 常量引用
普通引用只能绑定变量。常量引用可以绑定左值、右值、常量、字面值等任意形式。
绑定常量需要常量引用。
const int a = 10;
int b = 20;
const int& lr1 = 1; // 绑定字面值。
const int& lr2 = a; // 绑定常量。
const int& lr3 = b; // 绑定变量。
const int& lr4 = b + 1; // 绑定右值。
int& lr5 = a; // 错误:绑定常量需要常量引用!
3. 一维数组引用、二维数组引用、函数引用
// 1.一维数组的引用。
int a[5] = { 4, 3, 2, 1, 0 };
int(&ra)[5] = a;
// 2.二维数组的引用。
int b[2][3] = { {1,2,3},{4,5,6} };
int(&rb)[2][3] = b;
// 3.函数引用。rc是float fun(int, long)的引用。
float(&rc)(int, long) = fun; //fun加不加*都一样。
// 4.函数引用做参数。两种方式等价。testRc是参数为函数(引用),返回值为void的函数。
void testRc(float r(int, long));
void testRc(float (&r)(int, long));
sizeof(ra)、sizeof(rb) 是 a、b 数组的字节数 20、24。sizeof 的操作数不能是函数,所以没有 sizeof(rc)。
2.5 处理类型
3. 关键字
3.1 const 限定符
1. 作用
- 定义常量.
- 用于指针和引用:在类型说明符前用于绑定常量或者绑定而不修改变量,在类型说明符后彻底与对象绑定。
- 限定函数形参、返回值,防止修改。
- 限定成员函数:限定成员函数不能修改成员变量,const对象或成员函数只能调用const成员函数。
int a = 3, b = 7;
const int c = 9;
// 指针。
const int* pa = &a; // 不能通过pa修改a的值:*pa = 13报错;
const int* pc = &c; // 指向常量必须使用常量指针。
int* const pb = &b; // pb定义时必须初始化且不可再指向其它变量。
// 引用。
const int& ra = a; // 不能通过ra修改a的值:ra = 13报错;
const int& rc = c; // 引用常量必须使用常量引用。
int& const rb = b; // 这个const没必要,因为引用本来就是在初始化之后不能再被赋值。
rb = a; // 注意这是把a的值赋值给b,并不是让rb引用a。
2. 顶层 const 和底层 const
- 概念
顶层 const 表示变量、指针、引用本身是常量;底层 const 表示指针、引用关联的对象是常量。
int a0 = 1;
// 1.顶层const:限定非指针且非引用类型变量的const是顶层const。
const int a1 = 1;
int const a2 = 1; // const和int不分先后,这两行等价。
// 2.顶层const:指针、引用本身是常量。
int* const p1 = &a0; // 不能指向常量。
int& const r1 = a0; // 这个const没必要,因为引用本身就是顶层const。
// 3.底层const:指针、引用关联的对象是/相当于常量。
const int* p2 = &a1; // 无论p2指向常量还是变量,都不能通过p2修改所指对象的值,所以相当于指向常量。
const int& r2 = a1; // 无论r2引用常量还是变量,都不能通过r2修改所引对象的值,所以相当于引用常量。
// 4.混合类型。左边是底层、右边是顶层。
const int* const p3 = &a1;
- 拷贝
顶层 const 被初始化后不能再被赋值。顶层 const 对象本身(而不是指针或引用)给变量赋值时,const 没有影响。
int x = 1;
int* const p = &x; // 顶层const修饰的是p而不是p指向的x。
int* pa = p; // 1. 用p本身赋值pa实际是把&x赋值给pa。所以顶层const没有影响。
int* const *pb = &p; // 2. 把&p赋值给pb而不是用x赋值pb,顶层指针有影响。
赋值方有底层 const 修饰,被赋值方必须也有底层 const 修饰。
int x = 1;
const int* p = &x; // 底层const修饰的是x。
const int* pa = p; // 1. p认为x是常量。尽管x不是常量,用p赋值pa时,pa也要有底层const修饰。
const int* pb = &x; // 2. int*能隐式转换成const int*。
const int& r = x; // 3. int&能隐式转换成const int&。
3.2 constexpr
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
可以为常量表达式赋值:常量、字面值常量、constexpr 函数。
不可为常量表达式赋值:变量、没有 constexpr 修饰的函数。
可以为指针型常量表达式赋值:全局对象、静态变量。
不可为指针型常量表达式赋值:局部常量、字面值常量。
用 constexpr 修饰非常量表达式函数会出现编译错误。
3.3 volatile
volatile 涉及到程序的不可移植性,它告诉编译器不能优化这样的对象。volatile 和 const 有些类似,只有 volatile 的成员函数才能被 volatile 的对象调用。
合成的拷贝构造函数、移动构造函数、赋值运算符函数不能初始化 volatile 对象或从 volatile 对象赋值。
3.4 修饰符总结
- 不能再加:类外定义友元函数时不能再加 friend。 类外定义静态成员时不能再加 static。类外定义构造函数时不能再加 explicit。
- 必须再加:类外定义常量成员(静态常量数据成员、常量函数成员、常量返回值函数)时必须加 const。
- 可有可无:派生类定义覆盖基类虚函数的虚函数时 virtual 可有可无。