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