本菜菜子C++基础部分已经学过一遍,但教材用的是另一本书,并且学艺不精,很多地方掌握不到位,因此买了一本C++ primer(第5版)开始从新学习C++,让自己的基础变得更加扎实,也顺便重新审视一下这门难度非常大的编程语言。
至于为什么要记录,那当然是因为现在记忆力太差,即使是之前掌握非常牢固的知识,过一段时间肯定也会忘掉很多细节了,因此我觉得,随手写一点笔记,是必要的。
至于为什么从第二章开始,因为第一章一遍看下来没有太多需要记录的地方。第一章相当于一个大号的绪论,里面的东西在后面的章节都会详细介绍。
C++ primer(第5版)为新的C++ 11标准重新撰写,因此内容都是基于C++11的,C++11也是C++一次重要的变革,并且现在大部分编译器也能支持C++11了,所以我觉得现在可以直接学习C++可以直接建立在C++11的标准上。
下面开始正文,主要以记录为主,没想过会有人看。
目录
1、基本类型
1.1 内置类型
1.2 类型转换
1.3 字面值常量
2、变量
2.1 初始化与定义
2.2 标识符
3、复合类型
3.1 引用
3.2 指针
3.3 复合类型的声明
4、const限定符
4.1 引用与const
4.2 指针与const
4.3 顶层const与底层const
5、constexpr常量表达式
6、auto类型说明符
7、decltype类型说明符
8、自定义数据结构
9、总结
值得注意的是long long最小尺寸64位,是C++11中新定义的。
带符号的与无符号的:int,short,long long都是带符号的,加上unsigned就得到无符号的。unsigned指代的就是unsigned int。另外字符分为3中,signed char,char 和unsigned char,char与signed char并不是同一个东西。但具体区别没有提到,并且平时使用的表现形式只有无符号和有符号两种。
类型选择建议:
1、明确知晓不可能为负时选择无符号类型
2、整数尽量用int,超过了int选择long long
3、算数表达式不要用bool和char
4、浮点数用double,double与float代价相差不多(有些机器上double性能更好),但double精度更高
1、 把一个非bool的类型赋值给bool,初始值为0结果是false,其余结果为true。测试代码如下,结果现实只有0才会得到false的值,其余全为true
#include
int main(){
bool x = 1;
bool y = -1;
bool z = 0;
if (x)
std::cout << x << std::endl;
if (y)
std::cout << y << std::endl;
if(z)
std::cout << z << std::endl;
}
2、当把一个bool赋给非bool时,bool为false时赋予0,为true时赋予1,输出结果为l=1,r=0
#include
int main(){
bool x = true;
bool y = false;
int l = x;
int r = y;
std::cout << l << " " << r << std::endl;
}
3、浮点数赋值给整数时,会直接舍掉小数部分,当整数赋给浮点数时,则会将小数部分记为0
#include
int main(){
double y = 3.14;
int z = y;
double a = z;
std::cout << z << std::endl;//输出3
std::cout << a / 2 << std::endl;//输出1.5
}
4、给一个无符号赋予的值超出他的范围则会得到对最大值取余的余数。
#include
int main(){
unsigned int x = -1;
std::cout << x << std::endl;//输出结果为4294967295
}
5、给一个有符号赋予一个超出他范围的值,则是一个未定义的结果。可能产生垃圾数据。
6、含有无符号类型的表达式结果是一个无符号类型,例如
#include
int main(){
int x = 10;
unsigned int y = 32;
std::cout << x - y << std::endl;//结果是4294967274,-22对2的32次方-1取余
}
所以有符号与无符号混用时,带符号的会自动转化为无符号的,因此尽量不要混用。
字面值常量其实就是一个具体的值,之前我并没有真正理解他的含义,平时一直在用,但不知道字面值常量具体是一个什么东西。
以0开始的数字字面值是一个八进制整数,以0x开始则是十六进制。关于字面值的类型,十进制数默认是带符号的,而八进制和十六进制可以是无符号的也可以是有符号的。
#include
int main(){
int x = 024;//8进制
int y = 0x14;//16进制
}
严格来说十进制字面值只能是非负数,加上负号只是对值取相反数。
浮点型的字面值默认类型是double
字符字面值用单引号,字符串字面值用双引号,并且长度比实际内含的字符要多1,因为末尾有一个'\0'
std::cout << sizeof("123456") << std::endl;//输出为7
具体什么是变量就不细说了。
初始化与赋值是两个完全不同的操作,初始化的含义是创建一个变量并且赋予初始值。而给变量赋值是把对象当前的值抹去,并且用一个新的值代替。
1、列表初始化
初始化可以用括号,或者是花括号(列表初始化),或是直接用等号初始化,用花括号时,赋值对象不能存在信息丢失的风险,例如用double给int初始化会丢掉小数部分,因此这个初始化会被拒绝。
#include
int main(){
double ld = 114.514;
int x(ld), y = ld, z{ ld };//前两个正确,第三个不行
}
2、变量声明和定义的关系
声明就是声明,声明这个变量存在,如果一个变量想只声明而不需要初始化,那就使用extern关键字。一个变量只能被定义一次,但是可以被多次声明。
书中提到,变量命名要用字母或者下划线开头。不能出现两个连续的下划线,不能出现下划线接大写字母。(后两条规则我在visual studio 2022提供的C++ 20标准中测试过,这样命名并不会报错)
简要记录一下C++变量命名规则:
1、要体现实际含义。
2、一般用小写字母,自定义类名尽量用大写字母开头。
3、由多个单词组成的时候用下划线分割。
简单来说,引用就是一个对象的别名。只是为一个存在的对象取别名
1、引用必须被初始化,并且非常量引用只能绑定变量,不能绑定字面值常量。
2、引用本身不是一个对象,因此不能定义对引用的引用
3、引用的类型必须要和与之绑定的对象严格匹配,但是有两个例外(见后文,与指针的例外类似)
4、引用无法更改绑定的对象。
与引用类似,指针也实现了对其他对象的间接访问。
与引用的区别:
1、指针本身也是一个对象,允许赋值拷贝,并且可以先后指向不同的对象。所以可以定义指针的指针。
2、指针在定义时可以不用赋初始值,也就是不用初始化。
指针的类型必须要和与之绑定的对象严格匹配,但是有两个例外:
其一:允许一个指向常量的指针指向非常量的对象,引用也是如此,允许一个常量引用一个非常量,这样做不能通过指针或者引用修改变量的值
#include
int main(){
int x;
const int& y = x;
const int* p = &x;
}
其二,存在继承关系的类,可以将基类的指针或者是引用绑定到父类身上。
指针使用注意点:
1、对于解引用操作,只能用于那些明确指向了某个对象的有效指针,不包括指向nullptr的指针
2、指针如果是空指针,则作为布尔值使用时,值为0则结果为false,反之为true,比较两个指针是否相等,比较的是两个指针是否指向同一块地址。
3、void*指针是一种特殊的指针,可以接受任何类型的变量的地址,可以用作输出输出或是比较,但是不能直接操作void*指针。
变量声明包含一个基础变量和一组声明符,同一条语句基本类型只有一个,但是声明符行事可以不同。例如:
#include
int main(){
int x = 10;
int* p = &x, & r = x;
}
可以有对指针的引用,区分方法就是从右往左阅读,因为&离r最近,所以r是一个引用,然后看到int *知道是一个对int型的指针的引用。
#include
int main(){
int x = 10;
int* p = &x;
int*& r = p;
}
const应该是最头疼的一个部分了。const的作用是声明一个变量不能被改变。
首先const对象必须被初始化
如果用一个对象去初始化或者去赋值另一个对象,那么是不是const都无所谓。
默认状态下,const仅在文件内有效,因此如果要跨文件使用带const的全局变量,就需要用到上文提到的extern关键字。
对于常量,只能使用常量引用,对于变量,既可以使用常量引用,也可以使用普通引用。
#include
int main(){
const int x = 10;
const int& y = x;//正确
int& z = x;//错误,不能使用非常量引用绑定常量
int l = 15;
const int& r = l;//正确,可以使用常量引用绑定非常量
}
下面这样使用时错误的:
int l = 15;
int& r = l*2;//错误,表达式的值是常量
对const的引用可能引用一个非const的对象,这时不能通过引用修改对象的值。
1、指向常量的指针
指向常量的指针,顾名思义就是指向的对象是个常量,也可以指向非常量,但不能通过指针修改对象的值。
指针本身并不是一个常量,因此可以修改指针指向的对象
#include
int main(){
const int x = 10;
int y = 15;
const int* p = &x;
p = &y;
}
2、指针常量
指针常量指的是指针本身是常量,但是指向的对象并不一定是常量,因此可以通过指针修改所指对象的值,但是不能修改指针的指向。
#include
int main(){
int x = 15;
int *const p = &x;
int y = 10;
p = &y;//错误,指针本身是常量,因此不能修改指针的指向。
}
3、指向常量的指针常量
就是指针本身和所指对象都是一个常量,不能修改指针的指向,也不能修改所指对象的值。如果指向了一个非常量,那么也不允许通过指针修改对象的值
#include
int main(){
int x = 15;
const int *const p = &x;
int y = 10;
p = &y;//错误,不能修改指针的指向
*p = 20;//错误,不能修改所指对象的值
}
这也是非常绕的一个知识点。
但要分辨也很简单。
顶层const:指针本身是const,底层const:指向的变量是const。
个人有一个非常形象的记忆方法:将指针放在所指对象的上层,这时候指针所在的就是顶层,如果const是修饰指针,那const就是一个顶层const,如果修饰的是所指对象,那么就是一个底层const
#include
int main(){
int x = 10;
const int* p1 = &x;//底层const,指向的对象是一个const
int* const p2 = &x;//顶层const,指针本身是const
const int* const p3 = &x;//左边是顶层const,右边是顶层const
}
顶层与底层的应用主要是执行拷贝操作的时候。
拷贝时顶层const不受影响,也就是说可以用一个常量指针去给一个非常量指针赋值。顶层const在拷贝时会直接被忽略
#include
int main(){
int x = 10;
int* const p1 = &x;
int* p2 = p1;
}
底层const拷贝时有限制,不能用一个有底层const指针或者引用去给一个非const指针或引用赋值,但是可以用非const对象给一个有底层const的对象赋值
#include
int main() {
int x = 10;
const int* p1 = &x;
int &r1 = *p1;//错误,不能使用底层const去给非const对象赋值
const int& r2 = *p1;//正确,具有同样的const资格
}
constexpr就是常量表达式,与const类似,声明为const的对象必须被初始化,但与const不同的是,对constexpr的初始化必须用一个常量或者常量表达式。也就是说constexpr必须用常量初始化,因为在编译过程就必须知道constexor的值。
#include
int main() {
int x = 10;
constexpr int y = x;//错误,x是一个变量。可以给x增加一个const
constexpr int z = 114514;//正确
}
constexpr与指针:
constexpr用于修饰指针的话,相当于是一个顶层const,只与指针有关,与所指对象无关。并且指针的地址值也必须是一个常量。
#include
int main() {
const int x = 10;
constexpr int *y = nullptr;//正确
constexpr int *z = &x;//错误,x的地址不是一个常量
}
auto能自动推到变量的类型。所以auto类型必须有初始值。
一条auto语句的所有变量的基本类型必须一致,但复合类型可以不一致。
#include
int main() {
int x = 10;
auto* p = &x, y = x, & r = x;
//p是指向x的指针,y的类型是int,r是x的引用
}
auto会忽略顶层const,保留底层const
#include
int main() {
int x = 10;
const int* const p = &x;
auto p1 = p; //p1的类型是const int*
}
可以手动添加底层const
#include
int main() {
int x = 10;
const int* const p = &x;
const auto p1 = p;// p1的类型是const int* const
}
如果要声明一个auto是引用,那么要手动添加&,这一点与指针不同。原因是用引用来赋值是直接使用的是对象本身,因此auto只能获取对象本来的类型。而如果直接使用指针来初始化一个auto那么得到的就是指针类型,因为指针不解引用的话就是指针本身,如果用指针的解引用来初始化一个auto,那么auto的得到的类型就是对象本来的类型。
#include
int main() {
int x = 15;
int &y = x;
auto r1 = x;//r1的类型是int
auto &r2= x;//r2的类型是int&
}
功能与auto类似,也是用于自动推导类型,但与auto又有一些不同,使用起来比auto更加头疼。
简单使用如下
#include
int main() {
int x = 15;
decltype(x) y = 10;
}
与auto主要不同在于处理顶层const和引用,如果一个对象类型是有顶层const或者引用,那么decltype推导出来的类型也是一个顶层const类型或者引用类型。
#include
int main() {
int x = 15;
int y = 10;
int& r1 = x;
decltype(r1) l = y;//l是一个int&
int* const p = &x;
decltype(p) p1 = &x;//p1是一个int *const
}
如果想要通过一个引用得到对应的类型,那么可以使用一个表达式得到表达式的结果类型。下面的表达式如果不使用r1+0而是直接使用r的话,那么一定会得到int&类型
#include
int main() {
int x = 15;
int& r1 = x;
//现在想通过r1得到一个int
decltype(r1 + 0)y;//y的类型为int
}
decltype内的表达式如果是加上了括号的变量,结果将是引用。
#include
int main() {
int x = 15;
decltype((x))y=x;//y的类型为int&
}
注意decltype((x))的结果永远是一个引用,而decltype(x)则是当x本身是一个引用时才是引用。
C++11 允许在定义时为变量赋初始值了。
本次笔记记录了C++ primer第2章学习的大体内容,主要重点在于const与复合类型的混用,包括auto类型与decltype类型难点也集中在const与复合类型相关部分。因此我认为理解清楚const与复合类型的关系是本章的一个重点,另外一个重点就是掌握引用与指针的区别,其余重难点也基本都是围绕这两个方面展开。
最后,可能有写的不恰当的地方,欢迎指正。