C++ Primer学习纪录(一)
变量声明和定义的关系
声明(declaration):
语法:extern datatype name
声明仅仅是告诉程序,在其他文件中,已经有一个名为 "name"的"datatype"变量被定义
定义(definition):
语法: datatype name [= initial value]
定义不仅仅告诉程序本文件中有一个名为 "name"的"datatype"变量, 并且为这个变量申请一块内存空间,使其变为了实体(对象)
使用方法
// file: file_1.cpp
int buffSize = 1024; // 定义了一个名为buffSize的int类型变量
// file: file_2.cpp
extern int buffSize; //在file_2.cpp中声明了file_1.cpp中的buffSize,之后可以直接使用该变量
std::cout << buffSize;
output: 1024
声明的使用可以让不同文件之间的变量/常量可以互相使用, 是C++中分离式编译的一种机制。
复合类型
C++中基本复合类型为引用和指针两种
引用
引用:引用即别名。虽然在编译器中会给引用分配一个空间,但C++语言并不被认为引用是一个对象,因此引用的引用是一种错误的写法。另外引用必须在定义时进行对象的绑定,并且之后不会再被改变绑定的对象。
语法:datatype & name = init_value
注意点:
- 引用必须在定义时绑定引用对象,并且不可再改变
- 一般引用的类型必须与绑定的对象类型相同
int buffSize_1 = 1024;
int buffSize_2 = 2048;
double buffSize_db = 1024.5;
int &buffSize_ref1 = buffSize_1 ; // buffSize_ref引用了buffSize_1 对象
// 以下是错误写法
int &buffSize_ref2; // 引用必须在定义时绑定对象。
int &buffSize_ref2 = buffSize_ref1; //buffSize_ref1本身不是对象,无法被其他引用所引用
int &buffSize_ref2 = buffSize_db; // 引用的类型必须与绑定的对象相同,常量引用除外
buffSize_ref1 = buffSize_2 ; // 引用无法被改变,该写法试图改变引用所绑定的对象。
一些特殊用法
常量引用
语法:const datatype & name = init_value
对于常量的引用有一些特殊之处,即常量引用可以绑定非常量对象(变量对象),也可以绑定不同类型的对象。
const int buffSize_con = 1024; // 定义一个常量
const double buffSize_db = 1024.2; // double类型的常量
int buffSize = 1024;
// 常量引用定义
const int &buffSize_ref1 = buffSize_con; // 一般的常量引用类型
const int &buffSize_ref2 = buffSize; // 常量引用绑定变量对象
const int &buffSize_ref3 = buffSize_db; // 常量引用绑定不同数据类型的变量
cout << buffSize_ref3;
output: 1024;
buffSize_ref2
和buffSize_ref3
的绑定其实是做了如下的事情
// buffSize_ref2
const int temp = buffSize; // 编译器里申请的temp的临时空间用以类型转换
const int &buffSize_ref2 = temp;
//buffSize_ref3
const int temp = buffSize_db;
const int &buffSize_ref3 = temp;
注:尽管C++规定const
所修饰的常量引用不可修改值,但C++并没有限制常量引用必须绑定常量对象,故会出现对const
的引用并非const
对象的情况,如下所示。
int buffSize = 1024; // 定义变量
int &buffSize_ref = buffSize;
const int &buffSize_con = buffSize; // 常量引用绑定非常量对象
buffSize = 1025;
cout << buffSize_con << endl;
// output : 1025
buffSize_ref = 1026;
cout << buffSize_con << endl;
// output: 1026
即const
的引用值依旧改变了
指针引用
尽管引用本身不是一个对象,但指针却是一个对象,因此可以对指针进行引用。
语法:datatype *&name = pointer
读法:从右往左读,遇见的第一个标识符 &
代表该类型是一个引用类型, 第二个标识符 *
代表引用的对象是一个指针对象, datatype
代表对象类型。(其实个人认为使用datatype*
这种写法会更加具有可读性)
int a = 1024;
int b = 2048;
int *p = &a;
int* &r = p;
p = &b; // p指向对象b
*r = 0; // 通过 引用r 改变了 b 的值
cout << b << endl;
// output: 0
指针
指针基本操作
指针也是一个对象,在内存中会分配到一个空间,但其中存的是另一块内存的地址。
note: 指针操作主要有两个操作符
*
和&
, 而*
和&
在不同的位置代表的含义不同,以下做个区分
int i = 42; // 这是一个变量
int &r = i; // &紧跟 datatype , 因此是声明的一部分, &为引用修饰符
int *p; // *紧跟datatype, 因此是声明一部分, *为指针的修饰符
p = &i; // &出现在表达式中, 不是声明的一部分, 是一个取地址符
*p = i; // *出现在表达式中, 不是声明的一部分, *为解引用符
int &r2 = *p; // &是声明一部分,为引用修饰符;*不是引用一部分,为解引用符
note: 计组的知识告诉我们,对于4GB大小的内存而言,一个指针至少要占用32b = 4B大小的空间。
对于上述图,我们可以用代码来如此表示
int a = 1024; // 申请了地址为0x47FFFFFF~0x48000002的内存块存放int类型的变量a
int *p; // 申请了地址为0x95AC25FF~0x95AC2602的内存块存放指针变量p
p = &a; // 在0x95AC25FF~0x95AC2602的内存块中写入0x47FFFFFF
cout << &p << endl;
// output: 0x95AC25FF
cout << *p << endl; // *为解引用符, 即取到p所指向内存的对象中的值
// output: 1024
cout << p << endl; // p所存的值就是变量a的首地址
// output: 0x47FFFFFF
指针的基本状态
- 指向一个对象
- 指向一个对象的首地址(例如数组)
- 空指针nullptr
- 无效指针
空指针和void* 指针
空指针不指向任何对象,有三种方法生成空指针
int *p = nullptr;
int *p = 0;
int *p = NULL;
cout << NULL == 0 << endl;
output: true
建议使用nullptr
,避免使用NULL
void*
是一种特殊状态指针,可以存放任意类型的地址变量。以void*
的视角来看内存空间,我们看到的仅仅是内存空间,并不能确定这一块内存区域存放的是哪种类型的变量。
指向指针的指针
int ival = 1024;
int *pi = &ival;
int **ppi = π
读法: int **ppi =(int*)*ppi
从右往左读,定义了一个指针变量*ppi
, 该指针变量的类型(指向的类型)为int*
类型。
指向常量的指针
与引用一样,不可以通过指向常量的指针改变对象的值。
const double pi = 3.14;
const double *cptr = π // 指向一个double常量的指针
double pi = 3.14;
const double *cptr = π // 指向常量的指针可以指向一个变量
cout << *cptr << endl;
output: 3.14
pi = 3.1415926;
cout << *cptr << endl;
output:3.1415926
// 用指向非常量的指针 指向 常量对象出错
double *ptr = π // error C2440: “初始化”: 无法从“const double *”转换为“double *”
// 常量指针初始化出现问题
const int *ptr = π // error C2440: “初始化”: 无法从“const double *”转换为“const int *”
// 但是如上文所说,引用却会自动进行类型转化
const int &r = pi;
cout << r;
output: 3
总结:指向常量的指针和常量引用
- 指向常量的指针和常量引用都可以指向非常量对象,并且不可通过指向常量的指针和常量引用来改变非常量对象的值
- 常量引用可以指向对象类型不一致的对象,只要求这两个类型之间可以相互转换,但指向常量的指针不可以。
- 两者的"常量"两个字仅仅是指无法通过该复合类型去改变指向对象的值,但指向对象的值可以通过别的途径改变。
引用书上的一个小tips:
常量指针
tips: 注意区分 指向常量的指针 和 常量指针,这是两个概念。指向常量的指针是指所指向的对象不能通过指针修改,常量指针则是指 指针所指向的地址不允许修改。
double pi = 3.14;
const double *ptr = π // 这是 指向常量的指针
double *const curPi = π // 这是 常量指针
const double *const curPi_con = π // 这是 指向常量指针(常量对象的一种)的常量指针
const double &r = pi; // 这是 常量引用
常量指针的含义其实很简单,就是把指针本身定为常量。在上述代码中,也就是不允许改变curPi
所指向的对象,即
double pi = 3.14;
double pi_2 = 3.1415926;
double *const curPi = π
const double *const curPi_2 = π
curPi = &pi_2; //error C3892: “curPi”: 不能给常量赋值
*curPi = pi_2; // 改变指向对象的值正确执行通过
cout << *curPi << endl;
cout << pi << endl;
cout << *curPi_2 << endl;
output: 3.1415926
3.1415926
3.1415926
*curPi_2 = 3.14; // error C3892: “curPi_2”: 不能给常量赋值
总结: 常量指针的意思其实就是指针中所指向的地址(对象)不可被改变。 而指向常量指针的常量指针即指针中所存的地址和指向对象的值都不可以被改变。
指针引用见上文
const
修饰符
const
修饰符修饰的变量无法被改变,这样一个变量就变成了常量。
语法:const datatype name = init_value
const常量的定义及初始化
const int i = get_size(); // 通过函数定义,运行时初始化i的值
const int j = 42; // 通过字面量常量定义,编译时初始化j的值
const int k; // 错误写法,k未被初始化
note:
const
对象一般只在单个文件中有效,若是要在多个文件中共享const
对象,需要在定义和声明时都加上extern
关键字
不加extern
关键字(error
)
// file_1.cpp
const int N = 10;
// file_2.cpp
cout << N << endl; // error C2065: “N”: 未声明的标识符
const int N = 15;
cout << N << endl;
output: 15;
单个文件加extern
关键字(error
)
// file_1.cpp
const int N = 10;
//file_2.cpp
extern const int N;
cout << N << endl; // error LNK2001: 无法解析的外部符号 "extern const int N;"
两个文件都加上extern
关键字
// file_1.cpp
extern const int N = 10;
//file_2.cpp
extern const int N;
cout << N << endl;
output: 10;
结合一开始的定义与声明那块代码,可见
- 若是const常量在文件之间要共享,必须在定义时必须加上extern关键字
- 若是变量在文件之间共享,在定义时extern关键字可加可不加
- 养成好的习惯,在文件之间共享一个量,无论定义还是声明,均加上extern关键字
常量引用和常量指针见上文
顶层const
和底层const
顶层const
和底层const
区别一般是对复合变量而言的。
简而言之就是若一个指针/引用本身被const
限定符修饰,那么这个const
就是顶层const
;而若指针所指的对象被const
限定符修饰,这个const
就是底层const
。
再换句话说,const
修饰指针/引用则为顶层const
吗,修饰指针指向的对象则是底层const
int i = 0;
int *const p1 = &i; // const修饰指针p1, p1不可被改变,此const为顶层const
const int ci = 42; // const修饰常量ci, ci值不可被改变, 此const为顶层const
const int *p2 = &ci; // p2是指向常量的指针, const修饰ci对象, p2值可以被改变,此const为底层const
对于
const int *const p3 = p2;
第一个const
修饰p2
对象,表示p2
无法通过p3
指针来修改, 但p3
值在此层允许被改变, 此const
为底层const
;
但第二个const
修饰指针p3
限制了p3
的值不允许被改变,因此第二个const
是顶层const
顶层const
和底层const
的区别
主要区别是两种const
拷贝时不一样。
常量的底层const
不能赋值给非常量的底层const
。
对于顶层const
而言,拷贝时并没有差别
const int a = 10, b = 20;
int *const ptr = &a; // 此const为顶层const
int *const ptr_2 = &b; // 此const为顶层const
ptr = ptr_2; // 两个顶层const拷贝,不会有问题
const int *p = &b; // 定义一个底层const
p = ptr_2; // 正确运行,虽然const不同,但ptr_2的顶层const不影响拷贝操作
对于底层const而言,拷贝时必须要有相同的底层const类型或者可以转化为目标底层const类型
const int c = 0;
const int *p_con = &c; // 此const是一个底层的const
// 常量的底层const尝试拷贝给非常量的底层const
int *p = p_con; // error C2440: “初始化”: 无法从“const int *”转换为“int *”
const int *p = p_con; // 正确运行,因为p_con与p都是类型相同的底层const
int &r = c; // 运行错误, int&的类型与p_con的底层const不一样
const int &r = c; // 正确运行,int&和c有着相同的底层const
tips:若一条定义语句中既有底层const也有顶层const,由于顶层const不影响赋值操作,因此只需要关注底层const即可。 底层const的实质其实就是指向的对象是不可变对象(const常量),因此必须要用指向常量的指针来进行拷贝操作;而顶层const本质是指针指向的对象不可改变,因此对于新的指针/引用无须保证其对象本身的不可变性。
constexpr
const
修饰符在由函数赋值时,是在程序运行时赋值,编译器不负责检查。而constexpr
则是编译时进行赋值,由编译器进行常量表达式的判断。
constexpr int get_size(){
return 2048;
}
const int SIZE = get_size(); // 在程序运行时得到 SIZE = 2048, 因此此语句并不是常量表达式
constexpr int SIZE = get_size(); // 在编译时得到 SIZE = 2048, 因此此语句为常量表达式
这有什么用? 其实常量分为了编译期常量和运行期常量, 而const
并未区分出编译期常量和运行期常量,constexpr
则限定在了编译期常量。
那这又有什么用呢?在初始化数组的时候,必须给与数组一个编译期常量。同理的还有std标准库的array等容器。
对于以下代码
int get_size(){
return 2048;
}
const int size = get_size();
int Arr[size]; // error C2131: 表达式的计算结果不是常数
int Arr[get_size()]; // error C2131: 表达式的计算结果不是常数
VS2019会给出***error C2131: 表达式的计算结果不是常数***的错误 这是因为size
的值在编译时是得不到的,要在运行时才能得到,而Arr
在编译时必须要分配内存了,因此出现错误。
而对于constexpr
constexpr int get_size(int n){
return n + 2048;
}
constexpr int size = get_size(15);
int Arr[size];
int Arr[get_size(15)];
均能正确编译运行,因此constexpr
限定了编译时的常量,由编译器完成常量表达式的判断。
constexp
r修饰的指针
constexpr
只对指针有效,对指针指向的对象无效,这是constexpr
修饰的指针与const
修饰的指针最大的区别。
const int SIZE = 15;
const int *p = &SIZE; // 指向常量的指针, 该const为底层const
constexpr int *q = &SIZE; //是一个常量指针, 该const为顶层const
constexpr
修饰指针永远是一个顶层const
, 永远修饰指针对象而非指针指向的对象。
因此
constexpr int *q = &SIZE;
int *const q = &SIZE;
都可以用来定义常量指针。