第二章下
2024-7-13
这里特别是关于多文件的const在C++ primer中简单的带过去了,实际上理解起来需要用到其他像编译、链接的知识。
特点:
- 限定变量只读,因此使用时必须初始化
- 在多文件中共享const对象,该变量的定义和声明都添加extern关键字。对于const的实现,实际上是由编译器在编译过程中将const变量替换成对应的值,因此在多文件中,如果共享一个const对象,由于const对象都需要进行初始化,将造成重复定义的问题。
// file_1.cpp 定义并初始化了一个常量,该常量能被其他文件访问 extern const int a = GetValue(); // file_1.h 头文件中声明常量 a extern const int a; // 此时的 a 与 file_1.cpp 中定义的 a 就是同一个
这个时候自然考虑到只在一个文件进行定义,在其他文件进行声明,思考以下例子1
//a.cpp #include <iostream> extern const int i; int main() { std::cout<<i; return 0; } //b.cpp const int i;
这里会报错未定义 i 。
实际上关键在于了解extern关键字的作用:改变const变量的默认链接属性
//fileA.cpp extern const int i = 42; // 定义变量i并修改默认链接属性为全局可见 //fileB.cpp extern const int i; // 声明这个变量在其他文件进行了定义
在例子1中,a.cpp中声明了 a 定义在其他文件中,然而b.cpp中对 i 的定义对a.cpp是不可见的。
但我们还要考虑编译顺序的影响,考虑以下例子2
//1.cpp extern const int i; //const int i = 1; const int i = 1; //extern const int i; int main(){return 0;} //2.cpp extern const int i = 2;
只有按1.cpp中注释的写法能通过链接
按照编译的顺序检查文件,先 extern const int i,这时编译器会找这句之前是否对 i 进行了初始化,如果没有,则通过 extern 去文件外部,在 2.cpp 寻找到了对 i 的定义。这时,i 已经完成了定义。回到1.cpp后,发现了 const int i = 1,这时就会报错,重复定义:one or more multiply defined symbols found。按注释顺序,当extern const int i 时,已经在这个文件发现了对 i 的定义,不需要外部链接。
这里关于编译的知识还需要补一下
20240715
对const的引用(常量引用)
这时,引用也被视作常量,对const常量只能使用常量引用
const int i = 1; const int& cri = i; int& ri = i;//错误,类型不匹配
与普通引用不同的是,
- 常量引用允许类型不匹配或者使用表达式作为初始值,其遵循类型转换规则。
const double b = 1.2; const int& rb1 = b; const double& rb2 = b*2; std::cout<<rb1;//输出1 std::cout<<rb2;//输出2.4
特别的,能够为常量引用绑定一个非常量对象,这意味着不能通过该引用改变那个对象的值。
double b = 1.2; const double& rb = b; std::cout<<rb;//输出1.2 b = 2.2; std::cout<<rb;//输出2.2
指针与const
- 指向const的指针
这里的规则与上述类似,但是不允许类型不匹配的指针
const double i = 1; const double* cpi = &i; double* pi = &i;//错误,类型不匹配 const int* qi = &i;//错误 double b = 1.2; const double* pb = &b; std::cout<<*pb;//输出1.2 b = 2.2; std::cout<<*pb; //输出2.2
-
const指针(常量指针)
与引用不同的是,指针本身就是对象,因此可以把指针本身定义为常量。这就表示指针将一直指向该对象,指针值不能改变
int a = 0; int b = 1; int* const p =&a;//p将一直指向a p = &b;//错误 依然还是从右往左读判断声明。第一个const说明p本身是一个常量,不可修改其值,接下来的*表示p是一个指针,最后的int确定常量指针p指向了一个int对象。
顶层const、顶层const与拷贝
一般来说,其实只有底层const。而对于指针这种,本身是一个对象,又指向一个对象,才会用到顶层const和底层const的概念,。而引用本身不是对象,因此只有底层const
-
顶层const:表示指针本身是const
-
底层const:指针所指的对象是const
还是从右往左读
const int* a;//a是一个指针,指针指向的对象是常量int,因此不能修改指向的内容(*a),但是可以修改a的值 int const* a;//同上 int* const a;//a是一个常量,a是一个常量指针,指针指向一个int。因此不能修改其值,但是可以修改指向的内容 const int* const a;//都不能修改 int const * const a;//都不能修改
简单来说,const默认作用于其左边的内容,否则作用于右边的东西。
在拷贝操作中,两个对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够进行转换(非常量可以转常量)
20240716
constexpr和常量表达式
常量表达式:值不会改变且在编译过程中就能得到计算结果的表达式或对象,这由它的数据类型和初始值共同决定。
将变量声明为constexpr类型意味这个它的值是常量表达式,编译器将会对其进行检查,可修饰普通变量、函数、类的构造函数。
constexpr与const的区别:
const声明的对象不一定是常量,而只是“只读”,比如对非常量对象进行的const引用和const指针
int a = 1;; const int& ra = a; const int* pa = &a; std::cout<<ra<<" "<< *pa<<"\n"; a = 0; std::cout<<ra<<" "<< *pa;
这就说明了const修饰的对象可以在运行期间进行修改,而constexpr是在编译期间进行求值。
- constexpr指针:只对指针本身有效,而且必须初始化为nullptr、0、或存储于某个固定地址的对象,即必须是一个常量表达式
const int* pa = nullptr;//指向常量整型的指针 constexpr int* qa = nullptr;//指向整型的常量指针
处理类型
-
类型别名
typedef double wages;//wages是double的同义词 typedef wages base, *p;//base是double的同义词,p是double* 的同义词 //特别注意不能进行简单的替换 typedef char* pstring;//pstring的数据类型是指向char的指针 const pstring cstr = 0;//指向char的常量指针,也就是说不能改变pstr的数据类型 const char* cstr;//替换后变成了指向const char的指针 const pstring *ps;//一个指针,其对象是指向char的常量指针 -
别名声明
using wages = double;
auto类型说明符
由编译器自行推断数据类型,自然的,auto定义的变量必须有初始值。
auto会忽略顶层const,有需要时需要明确指出 const auto
不要滥用auto,Use auto to avoid redundant repetition of type names
decltype类型指示符
返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,但不实际计算其值。
decltype不会忽略顶层const
decltype与引用
这时必须进行初始化,特别由 decltype( ( variable ) )结果一定是引用