学习笔记 --《C++Primer》
0.前序
好吧那么...
是时候重修一下令我熟悉又陌生的C++了
自己是从C语言开始学编程的,学习Cocos2d时也使用C++进行编程
虽然知晓基本语法,但说实话对于C++了解并不深入,对于C11的一些新特性也是一概不知。对于即将展开的dx11的学习,以及以后的道路,我觉得,是时候看看这本书系统认真的学习一下C++了
1.一个简单的开始
---1.1 一个简单的C++程序
#include<iostream>
using namespace std;
int main(){
cout<<"Hello World"<<endl;
return 0;
这是一个最简单不过的C++程序,编译完成后系统会从这个main函数进入开始程序的执行。
这个mian函数中只有两条语句 cout<<"Hello World"<<endl; 在控制台打印一句传世经典Hello World 并换行
return 0; 返回一个整形数据0给操作系统,之后结束程序的运行
------1.1.1 什么是函数(方法)
这里提到了函数的这个概念,面向对象时称之为方法。编程中的函数与数学函数即有一些相似的点,也有一些不同的地方
编程中的函数(方法)是一些列指令操作的集合,通过调用函数(方法),结合传入的参数,以及函数运行时的环境,时机,来完成一个特定的功能
函数的定义包括四部分:返回值类型 函数名 参数列表 函数体
------1.1.2 对比编程中的函数与数学函数
数学中的函数是通过一定的映射,完成自变量和因变量的对应
比如函数:f(x)=x+1 给定一个x自变量,通过映射x+1 对应出一个值
下面用C++编程来完成一个数学上的f(x)=x+1 函数
可以看到传入参数1,最终输出显示结果为2
但不同的是编程中的函数(方法)并不局限于这样的映射关系,你可以将一个复杂的逻辑操作封装成一个函数,或是在一个函数中完成多项关联的操作,返回值也不必非得是int,一些函数的调用可能只是为了完成一些操作,并不需要返回值(void)
函数的存在就是为了对一些常用的操作,一些经常使用的功能进行封装,变成我们能够方便使用的工具,在需要的时候被调用
这里就不多展开了后面有专门的一章介绍函数,只说一点自己现在的再思考和总结吧
---1.2 C++ 中的输入和输出
不同于C语言中的标准输入输出语句(scanf printf)
C++使用一个标准库 iostream 来提供IO机制(Input Output)包括istream 和 ostream
stream(流)表示随时间进程的推移,字符产生顺序的消耗
------1.2.1 C++的标准输入输出流
#include <iostream>//引用标准库头文件
using namespace std;//引用命名空间
int main(){
int a, b;
cin >> a>>b;//标准输入流
cout << a+b <<endl;//标准输出流
system("pause");
return 0;
此外标准库还定义了 cerr 和 clog 分别输出警告错误信息和一般信息
------1.2.1.1 #include
通过#include引用需要的头文件,一般将#include放在程序源文件的最开始
编译时被引用的文件将会被编译器替换到对应位置
应避免错误的嵌套#include
后面会提到通过 .h 和 .cpp 的在cpp中引用头文件,定义实现分离式的编程
------1.2.2 向流中写入数据
<< 是向流中写入数据使用的运算符(后面会讲到重载运算符)
这里的<<要求左边和右边有两个运算对象,左端应有一个ostream(标准输出流)对象
右端为需要打印的值(一个变量或表达式)
该变量或表达式的运算结果会被写入到左端的ostream对象中
-----1.2.3 从流中读取数据
C++使用>>运算符来从流中读取数据
原理对照上面的使用<<向流中写入数据
这里就不细写控制台输入输出读取了,会用就好,因为我也不是个开发控制台程序的程序员,现在也不是开发控制台程序的那个时代了
-----1.2.4 命名空间
上面的代码中除了#include 引用了标准输入输出库
还有一个 using namespace std; 引用命名空间的操作
命名空间的存在是为了防止我们在编码时的命名冲突
不显式引入命名空间时也可以用 命名空间:: 来取出命名空间中定义的变量 函数(方法)进行使用
#include <iostream>
int main()
int a, b;
std::cin >> a>>b;//标准输入流
std::cout << a+b << "Test" <<std::endl;//标准输出流
system("pause");
return 0;
//不使用 std 命名空间仍调用其中的对象
定义命名空间
#include <iostream>
namespace myNameSpace1 {
int num = 0;
namespace myNameSpace2 {
int num = 0;
int main()
std::cin >> myNameSpace1::num>>myNameSpace2::num;//标准输入流
std::cout << myNameSpace1::num + myNameSpace2::num << "\nTestOver" <<std::endl;//标准输出流
system("pause");
return 0;
两个命名空间中各有一个名为num 的全局变量,尽管变量名相同,但它们并不冲突
当发生命名冲突时
发生命名重复时我们应该用 命名空间名:: 的来指定一个确定的变量或函数以完成调用
---1.3 使用while与for
这里就不做很细的记录了,感觉第一章为了大致介绍一下什么是编程,实现一个大致的功能,如此安排虽操之过急,但也不无道理吧...
后面的章节会详细解释各种概念,语法的,这里因为也不是完全没学过C语言我们就跟着它做一个大致的介绍和使用吧
------1.3.1 while语句
//while(循环条件语句) {
//被循环语句
while语句如上图 其中循环条件语句应是一个布尔值或是能过转换为布尔值的表达式
其实相当于while((bool)表达式),后面的for语句if语句中也有这样的特性
小心while(n=3)少写一个等号,相当于while(3),int 强转bool为true,导致死循环,且编译器不报错
这里从C语言过来的一个习惯性写法就是 while(3==n) 若真的少写一个等号,编译器将会报错“字面值3不能为左值”
while语句会不断执行循环条件语句,判定结果布尔值,为true执行while语句块(花括号{}引启的部分,后面会细说这个花括号和作用域),之后再返回执行循环条件语句。当为false时结束while语句
------1.3.2 for语句
//for(初始化语句;循环条件语句;循环增量语句) {
//被循环语句
for语句会先执行初始化语句,你可以在此进行赋值,定义for语句块中的局部变量(一般定义需要控制循环的变量)
之后执行循环条件语句,这里作用和while中的一致
之后衔接被循环语句,与while语句不同的是,被循环语句执行完后会执行循环增量语句,之后再返回循环条件语句执行,并检查结果bool值
------1.3.3 关于花括号
花括号其实括起了一个代码块,就如定义函数时,表示花括号中的代码为一个指令集,同时也确定了一个变量的作用域(后面章节会细讲)
当for while if 语句不写花括号时,会默认其后的第一条语句在循环/控制范围中
------1.3.4 使用循环语句读取不定量的数据
------1.3.5 细说缓冲区与输入读取
---------1.3.5.1 缓冲区的概念
首先是缓冲区的概念,缓冲区是一块分配好的内存空间,用于数据的读写操作。留存有用的数据,保留运算的结果。能在我们想要清空时被清空,想要更改数据时更改对应位置的数据,想要提取数据时提取出其中的数据。你可以把缓冲区理解成一张草稿纸,辅助我们留存数据,提取最终的运算结果
---------1.3.5.2 输入缓冲区
在C语言编写的控制台程序运行时,用户键盘输入的数据就被保留到一个输入缓冲区中。等待着例如cin scanf 一类的读取操作将数据读取出
或许有些编程老师会讲到,当遇到cin scanf时程序会停下来等待用户的输入(反正我在一开始学编程时网课老师是这么给我讲的)。 其实这么说有些不准确,跳过了输入缓冲区这个概念,cin scanf一类读取操作会尝试从输入缓冲区中读取需要的内容,如果输入缓冲区中没有内容,那么程序会等待用户的键盘输入,用户每次摁下回车时就将之前输入的字符压入到输入缓冲区之中
---------1.3.5.3 输入缓冲区的一个问题
注意:只要缓冲区中有内容,就会一直读取,不会停下,事实上这导致了C语言scanf输入字符(%c)时的一个问题:
ok我们换上C语言,这里scanf读取两个字符,我们先输入一个a给字符变量c1,之后摁回车。。。嗯?等等,为什么c2输入读取的也结束了???c2是...'\n'???
这里我们需要通过回车来将输入的字符压入到输入缓冲区中,但有一个问题是那个回车(字符'\n')也同样被压入到了输入缓冲区中。所以上面的连续读取%c就出现了问题,首先c1取输入缓冲区的字符'a',之后到c2读取,输入缓冲区正好还有一个'\n',于是就把这个值给了c2。为解决这个问题,C语言scanf读取字符时不得不读取字符串再做分解,或者每次读取字符前清理一次缓冲区...
---------1.3.5.4 cin与输入缓冲区
上面已经提到了cin读取时会自行判定缓冲区内容,确认流状态。如果在读取时遇到了不是预期的变量,那么就会截止读取,流状态就会变为false。因而我们可以用 while(cin>>)来连续读取,输入一个非预期的变量来截止读取
使用cin读取好的一点是没有'\n'的影响,cin不会读取'\n'字符
但其实cin没有了读'\n'的毛病,却还有另外一个问题需要我们解决,我们在上面的代码的基础上又增加了一些代码
可以看到流截断后我们再次使用cin读取数据时就遇到了问题,原因是在流阶段后cin的流状态转为false,不再读取数据
因此此时我们就需要手动恢复读取
综合上面缓冲区,以及cin的概念,我们需要做的事情有两件:
1.恢复流状态(从false转为true)保证后续读取的正常进行
2.此时输入缓冲区中有一个我们用来截断流的字符(上面的例子中就是那个字符'a'),我们需要将它清理掉,以免它的值被用到后面的读取
cin.clear();//恢复流状态
cin.ignore(numeric_limits<std::streamsize>::max(),'\n');
//提取(忽略)输入缓冲区中的字符直到'\n'为止
我们使用cin.clear来恢复流状态
使用cin.ignore(numeric_limits<std::streamsize>::max(),'\n') 来清楚截断字符
这里函数原型:cin.ignore(int,char); 作用:提取(忽略)输入缓冲区的字符直到提取量大于参数1的数量或提取到参数2的字符时
事实上我们希望此时cin.ignore提取掉缓冲区的所有字符,因此我们把第一个参数设一个尽可能大的值,起限制作用的只是第二个参数字符'\n'因为我们知道用户的输入是到\n截止的
这里numeric_limits<std::streamsize>::max()是流允许的最大字符量,你也可以输一个尽可能大的数字去替代,当然为防止那些找抽的用户拿上万字的字符串当截断符,我们用numeric_limits<std::streamsize>::max()是最保险的
所以记得每次流截断,读取之前,调用一下cin.clear() 和 cin.ignore(numeric_limits<std::streamsize>::max(),'\n')
ok那么记住这些就好了,再次声明我不是一个C++控制台程序员,如果你也不是那么记住这些就够了,主要介绍一下基本的流读取用法,和缓冲区的概念
---1.3.6 if语句
这里上面while语句里也有介绍,考虑到后面的系统学习这里就不细展开了
//if(条件语句){
//执行语句
//}
这里条件语句会被转换(强制转换)为bool值,条件语句可以是表达式,也可以调用函数(返回值为bool或能转换为bool),条件语句可以用&& || !这些运算符组合条件
比如判定平闰年的条件是:
bool isLerpYear(int y)
if((y%4==0&&y%100!=0)||(y%400==0)) { //每4年一闰,奉百不闰,奉四百再闰
return true;
return false;
else语句优先与上方最近的if语句配对
if语句不写花括号时默认其后第一条语句作为执行语句
此外 && 与 || 存在 熔断/逻辑短路
(语句1&&语句2),会先执行语句1,获取布尔结果,若为假,则语句2不执行,整个式子直接为假
同样的(语句1&&语句2),若先执行语句1为真,那么语句2也不执行,整个式子直接为真
---1.4 C++的缩进与代码风格
C++采用花括号来界定代码范围,因此相较Python这种以缩进区分代码范围的编程语言,C++风格就比较自由,你甚至可以把所有代码都写在一行,只要花括号没错
但事实是在实际编码中我们还是使用着缩进,留空白行和注释,来确保代码的可读性,代码不止是写给计算机的,也是写给人看的,对于那些“鬼畜”代码...在此就不予置评了
养成良好的代码风格对于一个程序员的个人编写,团队开发,以及后期的发展有着重要的影响,应聘时你的编码风格也会成为一个重要的参考项。 在读别人的代码时,我们都喜欢风格优良,注释简明扼要的代码,但对于个人的编写而言,也不必过度为别人的阅读考虑而影响你的编码速度,一个好的习惯约束就显得至关重要了
---1.5 浅谈类与面向对象
从面向过程到面向对象的转变,即使编程语言的一种进步,也是编程者设计思想上的一种进步
面向过程的编程即简单粗暴的实现需要的功能,在个人开发时为提高效率不失为一种选则
而面向对象的编程,意在将程序中的抽象概念设计为具体的对象(类型),以字段属性描述对象的特征,以对象的方法组合对象与对象间的行为,来构建程序的功能。面向对象的编程设计模式提供了更好的团队协作性以及更佳的扩展性,可修改性
尽管编程语言有所谓的是否面向对象,但面向对象更多指的是一种设计模式,设计思路,理论上用任何语言都可以实现面向对象的编程,这取决于开发者是否采用面向对象的设计理念,而不是使用了何种编程语言
这里推荐去看一下设计模式五大原则
C++中和其他面向对象的语言一样使用类类型(class type)来实现面向对象
第二章 C语言基础
2.变量和基本类型
提供一定的基本变量类型,是每个编程语言的基本。不同的基本类型用以描述最基础的一些离散事物
C++与大多数编程语言一样,除了基本类型,还赋予了程序员自定义变量类型的权利(Class Type 类类型),以及C++支持将函数定义在类类型的内部,作为类类型的一种成员
C语言中的结构体可以理解为对变量的封装,C++的类类型则在此基础之上提供了对函数进行封装的类类型
本章将了解C++的各种变量类型,C++的类类型,修饰类型的关键字,以及各种变量之间的转换关系
---2.1 静态数据类型语言
C++是一种静态数据类型语言 ,区别于动态数据类型语言(如:Python)
静态数据类型语言的特点是, 变量类型的检查发生在程序编译时 ,即程序运行时已经确定所有变量的类型。而动态数据类型语言,变量会在程序运行的过程中进行变量类型的推导
------2.1.1 使用 auto(var)关键字
为了编程时的方便,以及在某些特定的情况下程序员不必在意变量的具体类型。为此一些静态数据类型语言,演化出了一种定义不确定类型的关键字来完成变量的定义,C++中使用 auto关键字进行不确定类型的变量定义 (C#中使用var)
用这类不确定类型关键字来完成变量定义时,必须在定义变量的同时完成对变量的赋值,否则无法使用
auto visibelSize=Director->getViewSize();//定义变量同时赋值
因为本质上并不是使静态数据类型语言变为了动态数据类型语言,而是由编译器在编译时进行类型的推导,以确定变量的类型
---2.2 数据类型的重要意义
变量即保存了一定数据的,一片内存的代称
而变量的类型,就决定了我们应读取变量所对应的那片内存的多长的内容(变量占用的内存长度),以及应该如何解释那片内存中的数据(变量的类型)
比如一个int 整形变量 和一个int* 指向一个int 整形变量的指针变量。同样是32位比特,一个是一个整数,而另一个则是一个内存的地址
---2.3 数据类型基础 比特 字节 字
------2.3.1 数据的储存单元
老规矩,让我们先引入一些概念,以便我们去了解美妙的真相
时至今日,计算机能直接识别的仍然只有 1 和 0 这两个数据,1表示接通,0表示断开。只不过不同的是,和最早的图灵机比起来,去表示1和0的储存单元,已经从机械开关逐渐演化为如今的集成电路
计算机储存1或0的一个最小二进制储存单元被称之为比特(Bit)
而在内存中每8个比特位被放在一起成为1字节(Byte)
计算机内存的一个最小寻址单元,则被称之为1字(word)
我们通常所说的32位,64位就只
内存的1字由32位比特即4字节构成,或是由64位比特即8字节构成
1字的内存长度也是该情况下生成的程序中,任意一个最小的指针变量的长度(指针变量即保存内存地址的变量)
32位时指针长度位32位(4字节),也就是我们一般调试时(win32编译运行)的指针长度
------2.3.1 不同数据类型的数据长度
不同的数据类型可能对应了不同的内存长度,也因此它们所表示的数据类型的最大最小值有所不同。同一数据类型加上无符号unsigned 的修饰后数据量的最大最小值也会发生变换
并且在不同的环境下(32位或64位,Window或Linux),同一数据类型的长度也可能有所不同
事实上不同数据类型的长度和编程语言关系不大,许多基本数据类型是跨编程语言的
数据类型其实会有一个最小长度,但用处不大,其实记住数据类型的一般长度用处也不大,你所需要知道的是:
合理选用不同的数据类型去表示离散数据(可以参考我3D游戏数学中第一章1D数学的部分)
以及 整形一般选则 short,int,long long,确定不表示负数可以加unsigned 扩充正数范围
浮点型一般先选则 float 确定长度不够再用 double
---2.4 C++的各类基本数据类型
才...才不是因为人家懒才不更这里的呢...
就不多赘述了,嗯嗯
基本数据类型嘛...大家都知道的...嗯嗯
------2.4.1 有符号和无符号类型
无符号类型(unsigned)相比有符号类型,表示的数据量的大小有所区别,但相同类型的有符号和无符号内存长度是一样的(int 和 unsigned int 长度都是4字节)。有符号类型会有一个二进制位去表示符号(正负),也因此无符号类型就像是将有符号类型的负数部分也用来表示正数(short表示从 –32,768到32767 而unsigned short表示从 0 到65,535 ,65535=32767+32768)
所以加上一个unsigned 其实有着放弃负数表示,扩充正数范围的含义
unsigned int 可被缩写为 unsigned
事实上char类型也分为 signed char 和 unsigned char 具体使用时的char类型会是有符号或者无符号两者中的一种
---2.5 类型转换
变量会在相互赋值,函数返回或是一些特定的情况(比如 if while 条件判定语句中),变量的数据会在不同的变量类型之间发生转换
本质是对内存数据的一种转换,或是使用新的方式去解读数据
变量类型的转换主要分为隐式转换和显示转换两种
------2.5.1 隐式转换
隐式转换即没有采用显示的方法(调用转换的函数/方法,强制类型转换)进行的变量类型转换,或者说是编译器自行进行的类型转换
---------2.5.1.1 各类隐式转换的情况
隐式转换常在一些不经意的情况下发生
case 0: 相互赋值时
float f=3.5f;
int a=f; //将浮点数赋给整形,此时就发生了类型转换,以完成赋值
case 1:函数返回时
float fun(){
//...
return 1;//这里整形字面值1,进行了转换,之后返回
//...
case 2:函数传参
void fun(float);
int main(){
int a;
fun(a);//这里整形a转换为浮点型,之后传值
case 3:表达式中
float f=1.5+5/2; //这里f的值会是 3.5 而不是4 具体下面会细说
---------2.5.1.2 表达式的隐式转换问题
可以参与算术运算的变量被称之为算术类型,算术类型内部有一个算术类型的等级,进行算术运算时会根据算术类型的等级进行类型的转换和算术结果保留时变量类型的选择
表达式的混合运算中,每个单次运算结果会以参与该次运算的变量的最高运算等级的类型保留运算结果。如果学过表达式的树状结构,可以理解为每个操作符的运算后,结果会以下面的子节点中运算数据类型中的最高级保留并替换
其次不光是结果的保留,每单次运算在开始前会先把所有不是最高等级的数据转换到最高等级,再进行运算
这里的每单次运算指每单个运算符所进行的运算
比如上面的
float f=1.5 + 5/2;
首先 进行 “/” 运算 参与变量 5(字面值整形) 2(字面值整形),所以结果可以理解为是:int result=5/2; 或者(int)5/2 ,
单说5/2的算术结果,是2.5没错,但由于结果由最高等级的整形保留,浮点型2.5转换为整形时丢失小数位,所以程序中的计算结果会是 2
之后 1.5 + 2 结果由浮点型保留,会是正确的3.5
但这已经导致了表达式整体计算结果与算术结果4不符
因此,在计算“/”时一般字面值加.0 或.0f 写作 5/2.0f 才能计算出正确结果, 并且这是针对每个单次的运算都应这么做,而不仅针对整个表达式
非要进行两个整形变量相除 可写为 (a*1.0f)/b 来保证计算结果正确
再举一个例子
short s=-32768;//short 范围是 -32768 到 32767
直接输出 s-2 会得到-32770 因为字面值2是整形(int)等级高于短整型(short),先把s转换为int 之后再减2 结果用整形保留,所以能得到 -32770(int)
表达式中的隐式转换等级:
bool<char<整形<浮点型
整形内部,优先长度大的整形等级高,无符号比对应的有符号的整形等级高
同理,char内部 unsigned chat等级高于signed char,浮点型内部,长度大的双精度浮点型double 等级高于单精度浮点型float
---------2.5.1.3 各类需要注意的隐式转换结果
bool --> 整形:true转换为1,false转换为0
整形-->bool:0转换为false,除0外所有数转换为true
char-->整形/浮点型-->char:对应ASCII码表完成转换
浮点型-->整形:保留小数点前部分,小数部分丢失
整形-->浮点型:整形数据储存在浮点型的小数点前部分,若长度不够可能会有数据丢失,小数部分记为0
其中整形数据如果接收到的转换来的数据大小超过整形数据的表示范围,则整形数据结果会是该值对最接近的极限表示范围值相减后再加远一段表示范围的值,再根据数小过最小值加一,或大过最大值减一
若一次转换后仍超过范围,则不断转换直至数据转换到范围内,或超越大小(该值对最接近的极限表示范围值相减)先对范围长度(两个极限范围数据的值相减后的绝对值)取模后再计算
比如 short s=-32768;
s=s-2;
运算中,最终整型(int)-32770 转换到短整型(short)
short 范围 -32768到 32767
对-32770 近端 -32768 远端 32767
-32770小过最小值-32758,最后需加1
结果 (-32770 - -32768)+1+32767=-2+1+32767=32766
或者可以理解成一个周期函数的样式,最小值后以一位是最大值,最大值下一位是最小值酱紫
当遇到无符号类型时,就像是对无符号类型的最大值取模
---------2.5.1.4 判定条件的隐式转换
这里前面我们也提到了,if while for 语句中的条件判定语句,最终结果会被隐式转换为布尔值
if(n=3) 相当于 if(3) 同 if(ture)
---------2.5.1.5 UNSIGNED的问题
这里在其他数据类型被转换为无符号整形时,由于无符号整形只能表示0和正数
因此负数会被转换为一个正数,这经常引起错误
比如 for(unsigned short i=10;i>=0;i--) 是一个死循环,因为i永远不会小于0
又或者,由于同等级类型的无符号运算转换等级高于有符号
另一个问题是对于 vector.size() 这 获取容器容量 的方法,它的返回值是 size_type 亦是一个无符号类型
因此如果在循环中我们需要使用负下标来完成一些特殊的操作,那么和上面一样要当心出现死循环,应当先 int n = vec.size(); 再用有符号的n来写循环判定
因此使用无符号的表达式判定以及运算过程中,应特别注意
------2.5.2 显示转换
显示转换即由程序员显示调用某种转换方法从而完成的类型转换,包括强制类型转换,和一些标准库的转换方法,或自行实现的转换方法
---------2.5.2.1 强制类型转换
语法:(强制转换类型)待转换的变量
通过强制类型转换可以强制程序按我们想要的类型去解读内存中的数据,但强制类型转换存在一定的风险,尤其是强制转换导致的内存越界访问
但某些情况下我们又不得不去使用强制类型转换
---------2.5.2.2 调用标准库进行的转换
C++中内置 算术类型---> string 可以使用 to_string(Type e); 方法(C++11)
它的很多重载对应处理不同的算术类型
C#的话 则有 int.Parse(字符串) 读取字符串中的整形数据 以及 Convert.xxToxx 这类标准的转换函数
其实这里可以提一下C#中高精度转低精度是必须显示转换的,而C++中支持高精度到低精度的隐式转换
---------2.5.2.3 自行实现的转换方法
有时候还是自己造的轮子好用,自行实现的转换方法往往可以满足一些特殊的需求
下面是一个例子,自行实现的一个输入字符串转换为整数的方法,若输入错误则提示重输
int tryGetNum(const char * str)
int num = 0;
bool flag = false;
while (true)
cout << str << endl;
char str[16] = { 0 };
cin >> str;
for (num = 0, flag = false; str[num] != '\n'&&str[num] != '\0'; num++) {
if ((str[num] - '0' > 9 || str[num] - '0' <0) && (num != 0 || str[num] != '-')) {
flag = true;
break;
if (str[0] == '-'&&num == 1) {
flag = true;
if (flag) {
cout << "\n----错误,请重输!!----\n" << endl;
continue;
bool isnagNum = false;
if (str[0] == '-') {
isnagNum = true;
int iNum = 0;
int bit = 1;
if (isnagNum) {
for (int i = num - 1; i > 0; i--) {
iNum += (str[i] - '0')*bit;
bit *= 10;
iNum *= -1;
for (int i = num - 1; i >= 0; i--) {
iNum += (str[i] - '0')*bit;
bit *= 10;
return iNum;
---2.6 字面值常量
字面值即我们直接写在代码里的那些数据
int a=5;//这里这个给a赋值的数字5就是字面值常量
我们需要了解字面值常量的类型,以及如何显示的指定字面值常量的类型,以便我们更好的使用字面值,而不是仅仅不经意的“写了几个数字,几个字符串”
------2.6.1 整型字面值常量
---------2.6.1.1 各进制的整形字面值常量
我们可以指定整型常量的进制类型
20 //10进制
024//8进制 以0开头的数字字面值会被解释为8进制
0x14//16进制 以 0x 或 0X开头的数字字面值会被解释为16进制
C++中10进制的整形字面值常量会是int,long,long long中能容纳字面值的最小的那一个类型
而8进制与16进制类型则是能容纳其值的 int,uint,long,ulong,long long,ulong long中最小的哪一个
若一个整型字面值超过容量最大的类型,则会产生错误(编译时报错)
---------2.6.1.2 一个编译器的优化
与书中不同的是,我们使用VS时如果一个十进制整形长度超过long long了但可以被 ulong long容纳时,我们发现这个字面值的类型转变为了ulong long,我们仍可以正常使用这个字面值
所以这应该是VS编译器的一种优化,相当于默认后缀了一个u
不过经过VS中的测试,10进制的字面值在不断扩大时,前面的类型变化,的确是按照int,long,long long 类型递推的
---------2.6.1.3 整型字面值常量内存中的储存模式
上面有提到不同进制的字面值常量会被储存在不同的内存中,这里要提一下整形在内存中是如何保存的
首先内存中的数据是按照2进制储存的(这里经常弄混的一点是内存的地址是16进制)
因而其一:我们可以根据2进制的特点,以及整形变量的类型占用的内存比特长度来计算出该整形变量的表示极限
例一:
无符号短整形unsinged short 占用2字节 16位
2^16=65536 但16位全0时表示的是0 而不是1
所以unsigned short的范围 是 0到65535
例二:
有符号短整型short 内存占用和 unsigned short相同
由于有符号需要表示正数、负数和0,16位比特中有一位被用来表示符号
因此实际用来储存数字的有15位比特
2^15=23768 不过要从0开始,因此是0到23767,加上符号位就是-23767到23767
但其实根据第一位符号位的1(负)0(正)这里会出现一个+0和一个–0 但+0和–0是一样的
我们知道short 的范围其实是 -32768到32767,
所以为什么负数能多分到了那个原本属于0的一个位置?
这里需要解释的话就要提到补码了
度娘 :在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
有符号类型用第一位表示符号 1(表示负)0(表示正)
不过这里其实就有了一个+0 和 -0的问题 即原码:
0000000000000000 和 1000000000000000(16位)
但转换为补码去储存时 +0 和 -0 都转换为了0000000000000000(16位),然而+0是真的0000000000000000 而 -0是10000000000000000(17位)最高位溢出,因此16位显示回到0000000000000000,也就是说补码的1000000000000000(16位) 其实是 15位的-0,16位时没有一个数据的补码真的是1000000000000000(16位)
那么就产生了一个问题 就是补码的1000000000000000(16位)要去对应那个数值呢?
这里其实-32768 17位(1符号位16数位)表示为 1100000000000(17位)补码也是1100000000000(17位)去掉最高位符号位正好也是1000000000000000(16位)
因此就人为的规定了1000000000000000(16位)去对应-32768 也就是负数多一个的原因
其二:也因为内存中数据以二进制保存,我们判断一个任意进制的整数是否能够被一种整型数据类型容纳时,需要转为二进制进行判断
---------2.6.1.4 后缀改变字面值类型匹配列表
我们可以通过添加后缀来改变字面值的匹配类型列表(可利用后缀无符号来扩充正数范围)
1.后缀 u或U unsigned类型匹配(10进制字面值匹配类型将转变为 uint,ulong,ulong long)
2.后缀 l或L 最小匹配long
3.后缀 ll或LL 最小匹配long long
首先建议使用 L 和 LL 后缀 避免 l和1 的混淆
其次是我们进行后缀L和LL其实并不是定死了字面值的类型就是long 或long long,其加入后缀的实质是改变了整型字面值的匹配类型表,当数据长度超过 long或long long时依然会去匹配长度更高的类型
------2.6.2 浮点字面常量
浮点型字面值常量表示为一个使用小数点的小数或一个用科学计数法表示的数
指数部分用 e(指数值) 或 E(指数值) 来进行表示
3.1415926 1.56e14
---------2.6.2.1 浮点字面值的匹配类型
浮点字面值和整形不同,没有类型匹配列表,也就是说浮点字面值不会根据小数点前数值的增大,或是小数点后的位数增加而匹配更高精度的类型
浮点字面值根据有无后缀一定是一个确定的类型
默认情况下一个无后缀浮点字面值的类型一定是 double
后缀 f或F 类型变为 float
后缀 l或L 类型变为 long double
当小数点后位数增多超过浮点类型的小数点后精度时不会匹配精度更高的类型而是会产生精度的丢失,但在最高精度的末尾会进行一定的近似处理
特别的当小数点后出现过多的9且超过精度时,会被进位
---------2.6.2.2 浮点类型内存中的储存模式
首先我们都知道计算机其实只认识1和0,那么计算机中是如何保存小数的呢?那个小数点是被怎么记录下来的?
我们都知道一个科学计数法,314可以被写错 3.14×10^2 (3.14E2)
计算机储存小数也使用了这样的科学计数法,但不同的是我们常用的科学计数法是针对10进制的,而计算机内存中储存的是2进制数据,因而储存浮点数的使用的就是针对2进制的科学计数法
比如 2.125 转换为二进制小数就是:10.001(2--->10,0.125=1/8--->0.001)
10.0001转换为二进制的科学计数法就是 1.0001×2^1
那么计算机储存2.125 就分别储存 尾数部分: 0001(由于第一位一定是1所以不必保存)和指数部分 3+浮点型偏移量对应的二进制
这里引入一个浮点型偏移量的概念,国际规定在储存浮点型指数时(2的N次方时)应对应储存N+浮点型偏移量对应的二进制数。其中浮点型偏移量是2^e-1,e是储存浮点型指数位的比特位数
之所以要增加偏移量是因为存在负指数的问题,如0.75二进制科学计数法为 -1.1×2^-1
而指数位需要直接以原码储存的并没有引入补码,因为没有指数位的符号位
因此通过加偏移量2^e-1 就将可以表示的十进制数平分给了正负指数
以float为例 float占用内存4字节 32比特位 其中1位用来表示符号 8位指数位 23位尾数位
8位二进制能表示0到255的十进制数 而增加2^8-1=127的偏移量是为了让 -127表示为0,因此8指数位表示的指数范围就变为了-127到128
2.125 以float储存时:
2.125为正 符号位为0
二进制科学计数法表示为 1.0001×2^1
因此指数位储存 1+127=128 转二进制为二进制为 10000000
尾数位储存 0001
---------2.6.2.3 浮点类型的使用建议
由于精度的局限,以及实际运算可能的一些精度丢失,使用浮点型就意味着只能精确到一定的程度,而不可能绝对相等
因此在实际编码中应注意浮点型的精度问题
比如 判定浮点型是否相等,不要使用 == 而应使用相减小于一定容错精度值
if(f1-f2<0.0001){//0.0001作为容错精度值
//认为浮点数f1 f2 相等
在3D游戏中进行精确的移动和位置判定时,对浮点型进行近似处理到预设的精度范围 ,预设精度范围应结合实际需求以及浮点型数据的实际运行表现(特别的,如果是精确到个位 Math.Round()/unity: Mathf.RoundToInt() 是一个不错的方法)
由于3D游戏中的物体坐标都是以浮点数记录的,移动物体到一个确定的位置时, 除非特殊处理,否则物体永远不会移动到精确的位置上
这里一般的处理方法是当物体移动到距离确定位置一定的容错范围时,就认为物体已经移动到了该位置,停止物体移动(也可以在同时直接将物体位置直接设定为目标位置)
几种有问题的移动方法:
固定移动帧数,移动开始前计算每帧移动距离应用到之后的每一帧中,理论上这是一个好方法,但实际是我们发现由于浮点数的不精确,在浮点数想加减时某些情况可能产生误差,比如目标是 4 ,100帧完成每帧,移动0.25,但最终你可能得到3.99这个坐标(unity中我的确遇到过这种情况)。并且也有距离除帧数后结果的精度超越了浮点型的最高精度的情况。
固定移动秒速度,或帧速度,或固定移动时间,计算每秒/帧应移动多远,其实这是一个理论上都不可行的方法,因为游戏每次Update的执行间隔时间都是不一样的...除去浮点型的精度问题,和每帧执行间隔不相同的问题,移动距离在固定速度下,真的能在整帧完成的情况(整除情况)也几乎不存在
------2.6.3 字符字面常量与字串字面常量
以单引号引起的一个单一字符或一个转义字符 是一个字符char 类型的字面值常量
以双引号引起的一串字符是一个字符串 常量
字符串常量的储存是一个字符char 数组
---------2.6.3.1 字符与字符串的区别
字符类型是一个单字符,只占用一个char 类型的内存长度
而字符串类型则是多个字符的数组,占用所以字符数量+1个char类型的内存长度
字符串类型之所以占用长度是其中所有字符数量+1 是因为末尾有一个空字符'\0'
空字符'\0'是字符串的结尾字符
---------2.6.3.1.1 字符串的末尾'\0'问题
初始化字符串数组时 char str[0]={0}; 即所有字符初始化为空字符
在用 cin>> 读取C风格字符串时,字符串数组末尾会自行被压入一个'\0'
使用 sprintf 拷贝字符串时末尾会压入一个'\0'
strlen 字符串会得到该字符串的长度(第一个空字符前的字符数量,不计空字符)
遍历字符串时以'\0'作为结尾标志,自行实现的字符串拷贝方法需要注意末尾压入'\0'
格式化输出字符串时(cout<<str; printf("%s",str);)只会输出第一个'\0'前的内容
---------2.6.3.2 指定字符类型
可通过前缀来指定字符类型对应的字符集,也一些前缀会同时改变字符的变量类型
前缀u 对应Unicode16 字符集 类型char16_t
前缀U 对应Unicode32 字符集 类型char32_t
前缀L 宽字符 类型wchar_t (这里要提到的是一般情况,一个英文字符占用一个char而一个中文则占用两个char,宽字符是无论任何字符都同一占用两个char)
前缀u8 对应UTF-8字符集(仅用于字面值常量) 类型char
---------2.6.3.3 转义序列
一些字符在C++中是有特殊含义的,或者说其本身构成语法的一部分 如:
单引号' (引起字符类型变量)
双引号“ (引起字符串类型变量)
反斜线\ (引起转义字符)
问号?(三目/条件运算符)
而这些符号本身若需要表示为字符类型或在字符串中使用 就需要在前面加一个反斜线 \ 来取消这类字符的语法意义
虽然某些情况下不加\也能正确输出,但我们十分不推荐这么做,
请一定记得加\,用以应对可能的出错
---------2.6.3.4 转义字符
转义字符是指那些不可被直接打印(格式化输出)的,用于控制格式化打印或在被格式化输出的同时产生一些特殊效果的
常见的转义字符:
\n 打印换行
\t 横向制表符
\v 纵向制表符
\a 响铃
\r 回车
\f 进纸符
---------2.6.3.4.1 使用泛化的转义字符
可以用反斜线\跟一组8进制数(无序0开头)对应泛化的转义序列
但仅使用反斜线\+数字时只读取后3位数字作为8进制数对应字符集的转义字符,若想读取后面的所以数字应使用\x+数字,但超过3位8进制数的转义字符在char字符仅占1字节8比特的一般环境下会出错
---2.7 变量
------2.7.1 变量的定义
C++的变量定义以变量的类型说明符(变量类型关键字)引起,空格后紧跟一个变量标识符,或以“,”间隔的多个标识符,分号结尾,完成定义
------2.7.1.1 标识符与命名规范
标识符规则:
C++定义变量时所使用为变量命名的一串字符即标识符
标识符包括数字,字母,下划线(不可使用特殊符号)
标识符的字母不限制大小写,但对大小写敏感,(int abc,aBc,abC; 定义了三个不同的变量 )
标识符必须以字母或下划线开头,不可以用数字开头
标识符不可以和C++的关键字重名
命名规范:
“对于命名规范来说,若能坚持,必将有效”
命名规范是一种约定俗成的规矩,尽管不遵守也不会导致程序无法运行,但坚持命名规范可以提升代码的可读性,编码的规范化,以避免一些不必要的错误
常见的命名规范包括:
变量小写,局部变量下划线开头,自定义类型大写开头
全局变量 g_ xxx, 成员变量 m_xxx
使用“_”或驼峰区分字母
顾名思义,不要缩写过多单词
------2.7.2 变量的初始化
初始化的意义通过在变量的定义之初就为其赋予一个初始值,保证变量能够正确的参与后面的运算
变量定义的本质是根据变量的类型,分配一定的内存(C++中变量内存的分配在定义变量时),将变量和那片分配好的内存连接起来,而初始化就是在分配内存后立即对内存中的数据进行处理
比如我们常在定义指针变量时将其初始化为指向的对象,暂时不指向任何对象时初始化为nullptr,结合我们在运算前 if(ptr){ } else{ } 这类的判定,保证我们不会错误的调用一个空指针或野指针
其实在C#中还有这样的要求,以out关键字传入的变量必须在函数体内完成初始化,以及不能引用未初始化的值类型(C#中值类型变量内存的分配发生在初始化时)
---------2.7.2.1 初始化与赋值
int a=10;//定义变量a,并初始化为0
int b;//定义变量b
b=10;//变量b赋值为0
其实我们应该将初始化和赋值区分开,使用=不一定就是在赋值,它也是初始化的一种方法
但并不是 int a=10; 和 int a; a=10; 这样会有区别,如果你在定义后不进行任何其他对变量的操作,并立即赋值,那么其实也算完成了变量的初始化
---------2.7.2.2 显式初始化的4种方式与区别
显式初始化共有四种方式,以四种不同的操作符进行区分
其中使用=的初始化是通过赋值来完成的初始化
使用()圆括号进行初始化是进行拷贝初始化,对于类类型而言或调用类类型的 拷贝构造方法 来进行初始化,具体后面会进行补充
使用{} 和={} 则是通过初始化列表来进行初始化,C++11之前初始化列表一般只用于数组变量的初始化,而C++11后初始化列表被全面的应用
---------2.7.2.3 初始化列表
使用初始化列表进行初始化时如果存在丢失数据的风险,编译器将会报错,而赋值初始化和拷贝初始化则只会进行警告,程序依然可以运行
后面会补充其他初始化列表所能实现的功能
---------2.7.2.4 类类型的初始化列表
在类类型的构造函数中我们可以在参数列表的圆括号后,函数体花括号前以 ":"引起 “,”间隔对类类型的成员变量进行通过()的拷贝初始化,或列表初始化(但不可以进行=赋值初始化,并且通常我们使用拷贝初始化,以便正确调用成员类类型的初始化方法)
并且需要注意的是,初始化的顺序是按照类类型中成员的上下排序进行的,上图中尽管 mName(name)写在前面,但先初始化的是m_Age
下面再举一个例子:
并且我们还可以发现,这种列表初始化的执行是在执行类型初始化方法之前,上图中先执行了MTypeA 和 MTypeB的初始化方法,之后才执行MyType的初始化方法
使用类类型的列表初始化可以提升运行的效率
---------2.7.2.5 默认初始化(非显式初始化)
上面提到了4种显式初始化的方法,但若在变量定义时不进行初始化,那么将进行变量的默认初始化(非显式初始化)
------------2.7.2.5.1 内置类型变量的初始化
像int float 这样的内置类型如果在函数体内定义(局部定义)那么它们不会进行初始化,上面有提到定义变量时会为变量分配内存,而不进行初始化会导致内存中残留的上一次使用后遗留的数据被错误的按照变量的类型进行解读,这也是野指针产生的原因
而函数体外进行定义的变量(全局定义),如果不进行初始化,默认初始化会对其内存数据执行“清0”操作,即所有二进制比特位上的数据全部置为0
------------2.7.2.5.2 类类型的默认初始化
类类型定义时如果不进行初始化,那么会调用该类类型的 默认构造方法
默认构造方法是一个没有参数列表的构造方法,
如果类类型没有显式定义任何构造方法,那么编译器会为其自动添加一个无参数列表,无任何语句的公开的默认构造方法
一旦显式定义了类类型的构造方法,那么那个默认添加的构造方法就会无效,但你仍可以显式定义一个无参数列表的构造方法作为默认构造方法,并且进行其他默认初始化操作
类也可以通过在定义成员字段时对字段进行初始化,这被称为 类内初始值 ,用以代替默认的内存清0(但一般不建议这么使用)
通常我们通过设定构造函数,传入参数决定在创建类时可以使用的初始化方式
如果一个类没有任何显示的构造函数定义,或是调用了未对成员变量进行任何操作的构造函数,那么其成员变量也会被 默认执行的“内存清0”操作 初始化
---2.8 声明与定义
这里要理解两个概念 声明 和 定义
声明是告知编译器有一个变量、函数/方法,并且要之后的运算中使用(先声明后使用)(There Should Have a xxx)
而定义是具体确定变量、函数/方法,定义时会进行内存空间的开辟,变量根据作用域分配不同的内存空间,函数/方法分配代码段的空间(The xxx is x&^#!xx)
可以多次声明,但定义只能有一次
定义的同时也完成了声明,但声明并没有进行定义
------2.8.1 C++的分离式编程
C++中我们通常将声明写在头文件(.h)中,将定义写在源文件(.cpp)中,通过一组同名的头文件和源文件,在源文件中完成对头文件中变量、函数/方法的定义。在需要使用某些变量、函数/方法时包含(#include )声明了它们的头文件
这样我们就保证只在对应的源文件中进行了唯一的一次定义,而通过预编译时对头文件的展开替换,我们就将头文件中的声明替换到了我们需要的地方
/*
当然...除了模板类那个败家子...(手动滑稽)
因为模板类需要进行不定类型的内联,所以分离式会导致模板类的不定类型无法完成内联确定
*/
------2.8.2 变量的声明与定义
变量的定义上面已经提到通过类型说明符引起,空格并跟一串标识符,通常在定义的同时完成初始化
而变量的声明通过关键字 extern+类型说明符 以及表示符来完成,不可以再声明时进行初始化,否则将视为定义变量
extern int a;//声明变量a
extern int a2=10;//定义变量
通常我们在函数体内进行局部变量的定义,而 对于全局变量,我们应在头文件中进行声明,并在对应的源文件中进行定义 ,并且为避免重名,可以将全局变量定义在命名空间内
一个常见的错误是我们将全局变量定义在了头文件中 ,此时若有两处及两处以上的的地方引用了头文件,根据预编译的展开替换,一个定义就被替换到了两个地方,将出现多重定义的报错
------2.8.3 作用域与生命周期
//两个概念
//1.作用域对变量声明,定义,引用的关系
//2.作用域对生命周期的影响
在定义变量时,根据变量所 声明的位置 ,变量就被定义在了不同的作用域。其实不光是变量,我们所 声明 的函数,类,根据定义的位置也被赋予了不同的作用域,所以我们常说“ 某个声明的作用域是... ”
前面已经提到,在C++中,由花括号所括起的范围(可能是一个函数体,也可能是if while for着类控制语句)就是一个作用域。如果一个 声明 不在任何花括号中,那么这个 声明 就 是全局 的,或者称这个 声明在全局作用域中
程序在执行的过程中,根据不同函数方法的调用,以及控制语句的执行, 作用域会进行不断的切换,相互的嵌套
---------2.8.3.1 作用域对变量声明、定义、引用的影响
当超出作用域时,就无法引用作用域内所声明的变量
当作用域出现嵌套时,小局部作用域可以覆盖大局部的同名变量的定义(或者参数列表覆盖高一级的变量定义)
同级作用域内 不能重复定义同名变量 ( 但可通过命名空间来解决这种情况 )
也不允许声明已经在该作用域中定义过的变量
在重名覆盖时,可以通过 "::" 双冒号来特定的调用全局作用域中的全局变量
---------2.8.3.2 extern关键字
前面已经提到,通过extern关键字+类型说明符,不进行初始化,可以声明变量
extern关键字的存在是为了我们能够正确的声明和定义全局变量,避免多重引用头文件时造成的多重定义的错误
extern关键字的作用是告知编译器这里应该有一个变量,它被定义在别处(别的文件中)请将它链接到它的定义(C++定义时分配内存,而内存中保持了这个变量的数据),以便后续正常的调用
而查找定义的范围是在全局作用域中, 而同级作用域中不允许重复定义,因此能保证链接到一个唯一确定的变量
如果链接不到定义且后续进行了调用,那么会在编译时报错,“无法解析的外部符号”
事实上编译器借由extern关键字的链接查找定义的作用比我们想象中的强大,它会查找整个项目中的全部文件的定义,即使我们没有通过include包含, 只要定义变量的文件在我们的工程目录中,编译器就会找到并链接到定义
---------2.8.3.3 作用域与命名空间
同级作用域内不能进行重命变量的定义,使用命名空间是解决变量重名问题的好方法
我们可以将变量、函数/方法声明在头文件的命名空间中
并在对应的源文件中引入命名空间,并进行变量、函数/方法的定义
引入需要的命名空间,进行变量、函数/方法的调用,出现不同命名空间有重名定义时,编译器会报错“变量(函数/方法)不明确”,此时我们应通过 命名空间:: 的方式进行调用
---------2.8.3.4 作用域对生命周期的影响
当执行到 "}"结束一个作用域内的所有代码时,部分在作用域内定义的变量会被销毁,其内存会被回收(储存在栈中的变量被销毁)
---------2.8.3.5 不同类型变量的生命周期
------------2.8.3.5.1 全局变量
//static const静态,非静态
全局变量在程序运行的整个过程中不会被自动销毁,它们具有永恒的生命周期,你可以在任何拥有全局变量声明的位置调用全局变量
但要注意的是 静态全局变量只能在当前源文件内被调用,而非静态变量的作用域是全局的,因此静态化全局变量会改变它的作用域
------------2.8.3.5.2 局部变量
//堆,栈,局部静态
局部栈变量在结束作用域的全部代码时会被销毁
堆变量是需要程序员手动管理的,因此除非使用调用 free 或 delete 方法,堆变量不会被销毁
任何静态变量会在程序运行时被放在寄存器中,具有永恒的生命周期 ,因此局部静态变量不会被销毁,但你只可以在局部(有限的作用域中)调用它。 静态化局部变量,会改变它的生命周期
---2.9 复合对象
------2.9.1 复合对象的基本概念
复合对象是 基于基本类型的一种对象类型 。复合对象通过一个基本数据类型和紧跟其后的一个 声明符列表 组成,不同声明符的决定了该复合对象基于该种基本类型所扩展的功能
------2.9.2 引用
C++中新增了“右值引用”,将在之后提到
这里的引用是指“ 左值引用 ”
引用通过 基本数据类型 & 变量名 = 初始化需要绑定到的对象
来完成定义
引用会和变量绑定在一起,通过它我们可以访问它所绑定到变量,读取变量的数据或将数据写入变量
引用必须在声明时进行初始化,将其绑定到所需要绑定的对象上。初始化后,引用不能重新绑定到别的对象
引用本身不是一个对象,因为它只是它所绑定的变量的一个别名。通过引用去初始化引用,只是将被初始化的引用绑定到了相同的对象上。没有引用的引用
除过两种特例 (后面会再提到), 引用与其绑定的对象必须在类型上匹配 。引用只能绑定在对象上,而不能被绑定到某个字面值或表达式的计算结果
---------2.9.2.1 引用的使用
引用不是一个对象,它没有被分配到内存,但可以通过它来访问被绑定的对象
使用引用进行参数传值 是一个常见的用法。引用传值相比指针传值,或是值传值更加高效
引用作为返回值 也是一个常见的用法,通过 xxx&=fun();来完成接收。这么做的一大好处是不会产生返回值的副本,从而提高效率
但要注意的是返回引用, 外部调用时的接收位置是否已经超越了引用所绑定对象的生命周期
绝对不要返回局部变量的引用 (除非是局部静态变量),由于非静态局部变量会在运行后被销毁,导致返回的引用没有意义
一般不要返回局部通过Malloc New 创建的变量 ,这可能会妨碍对内存的回收
可以返回类成员的引用 ,进而在外部修改类成员
一些 只读的get方法 可以 使用引用返回来提高效率,并加Const限定修饰
尽量返回全局变量或是静态变量的引用
------2.9.3 指针
指针是 记录对象的地址 的一类复合对象,指针对象通过 基本对象类型 * 变量名; 来完成定义, 定义指针的时候可以不赋予初值,但最好初始化指针为nullptr
指针有两个重要的概念, 指针的类型 和 指针指向的类型
与引用不同, 指针本身是一个对象 ,它有被分配内存(我们有提到指针对象会被分配当前环境下内存最小寻址单元 1 字的内存长度)
指针对象可以在其生命周期内指向多个不同的对象,通过&操作符可以获取对象的地址赋予给指针(这经常和定义引用时的&混淆,但注意&出现在=的左侧还是右侧可以加以区分),不能多次&操作
除两种特例 (后面会提到)以外,指 向的类型需要和赋值时地址的类型匹配
通过*操作可以访问到指针所指向的对象 ,其底层实现是通过指针对象储存的地址,寻找内存对应位置的数据,再将数据按照指针指向的类型进行解读。 可以进行连续的*操作,多层寻址访问
---------2.9.3.1 指针值的问题
---------2.9.3.1.1 指针状态
指针会有4种状态:
1.为空
2.指向了一个有效的对象
3.指向了紧邻对象的下一个位置
4.无效指针
---------2.9.3.1.2 nullptr与NULL的区别
在C语言中NULL 被定义为一个((void*)0)
而到了C++中, C++不允许void*被隐式转换,C++中的NULL直接被定义为0
因而 C++中引入了nullptr来表示空指针替代NULL ,nullptr的类型是nullptr_t,能够被隐式转换为任何指针
虽然空指针是内存中数据全为0的指针,但 不可以用一个为0的int型变量来进行赋值将指针置空
---------2.9.3.1.3 关于指针值的一些操作
指针可以直接被作为if语句判定的条件
if(ptr) 相当于if(nullptr!=ptr),即当指针不为空时判定为true执行if所控制的语句,指针为空时判定为false不执行if所控制的语句
可以用 == != 对指针进行判定 ,判定结果依照指针的值是否相同,即 指针所保存的地址是否相同 。但 某些情况下指向一个对象的指针可能和指向紧邻对象下一位置的指针被判定为相等
可以用 > < 对指针进行判定,判定结果依据指针所指内存地址的高低。其中栈内存是从高到低分配的,而堆内存是从低到高分配的
可以用 + - 操作来移动指针,移动指针是根据 指针指向的类型所占的内存长度乘 + -指针的整数长度 获得一个 指针移动的内存距离 ,将指针所指的地址替换为原地址+移动内存距离后的地址
void* 指针无法进行+ - 移动操作 ,因为void*指针没有指向的类型,无法确定内存移动距离
---------2.9.3.1.3.1 *与++/--
请一定不要使用那些不标准的,很容易产生歧义的写法 ,将其替换为标准的写法是一个好习惯
编程是完成人类与计算机之间的交流,代码不只是给计算机,还是要给人去看的
至于更加鬼畜的多重组合就不列出讲解了...这需要运算符优先级,结合顺序的知识进行解读,但除了作为考题外就没有任何意义
1.*(p++)/*(p--)
比较标准的写法
由于是后自增/减 故先访问*p 之后进行指针移动
可以展开为更加常见的 访问*p 之后p++;/p--;
2.*p++/*p--
不标准,很容易产生歧义
*p++/*p-- 其实相当于case1中 *(p++)/*(p--)
3.*(++p)/*(--p)
标准的写法
由于是前自增/减 故先进行指针移动,之后访问
4.(*p)++/(*p)--/++(*p)/--(*p)
标准的写法,经常使用且不容易产生歧义
该写法是访问指针指向的对象并对其进行 前/后 自增/自减 操作
5.++*p/--*p
不标准的写法,容易产生歧义
其实这种写法等效于 case4中 ++(*p)/--(*p) 但我们应加上括号,避免产生歧义
6.*++p/*--p
不标准的写法,容易产生歧义
这种写法等效于 case3中 *(++p)/*(--p) 先移动指针,再访问
---------2.9.3.1.4 访问有效的指针以及无效指针访问的后果
无效指针有一个别称“ 野指针 ”
所有对指针的操作都应该保证不使用无效指针
因为无效指针不为空,但却指向了一个无效的内存区域。 我们无法通过if判断区分出指针是否无效,编译器也不负责检查无效指针 。无效指针可能是由于对象的释放导致指针的无效,也可能错误解释了内存中的数据,或是获得了错误的数据
实用无效指针会导致访问到了一片本不应被访问到的内存区域,这可能成为错误数据的来源,更可怕的是如果修改了无效指针指向的内存中的值。 可能会出现严重的飘忽不定的bug,或是程序甚至是操作系统的崩溃,虽然部分编译器有越界访问保护,但对工程在实际运行时并不生效
我们应保证每个指针的有效,指针应指向一个有效的对象,或是在指针所对应的那片内存被释放后立即将指针置为空
在定义指针时立刻进行初始化(局部变量没有默认的内存清0初始化),将其指向一个有效的对象或者将其置为空
熟悉变量的生命周期,不要使用无效指针做参数,更不要返回一个无效的指针
适时通过 if(ptr) 在我们进行访问前判断指针是否为空再进行访问,或是在为空时进行其他操作
---------2.9.3.2 void* 指针
void*指针即 只记录地址,一个没有指向类型的指针 。因而被叫做空指针
void*指针所能做的只是提供一个地址,在 C++中我们可以通过强制类型转换将void*提供的地址进行一定的解释
事实上malloc的返回值就是一个(void*)
---------2.9.3.3 指针的指针
指针本身就是一个对象,因此有指针的指针
再学习时许多教材用一级指针代表指向了一个对象的指针,二级指针代表指向了一个一级指针的指针...n级指针代表指向了n-1级指针的指针
但 这种n级指针的解释模式其实并不易于学习编程的人去理解指针的概念
我们前面提到指针有两个概念, 指针的类型 和 指针指向的类型 ,也有讲到 变量的意义即编译器解释变量所对应的内存中的数据的形式
真正理解指针其实用到的就是这两个概念
指针保持的数据编译器会将其当作一个地址, 指针的类型取决于在获取地址时地址的类型应当与指针的类型对应 ,而指针指向的类型其实就是 (*指针的类型)所对应的类型
编译器会根据指针指向的类型将指针所指向的内存地址中的数据进行对应的解读
一级指针,编译器会将指针指向的那片内存地址上的数据解读为一个变量
二级指针,编译器则将那片地址上的数据解读为一个一级指针
因而我们需要注意 避免使用无效指针的原因也是因为需要避免编译器去访问到一片错误的内存,又将内存中的数据错误的的解读,错误的修改
还要补充的是,我们有提到指针的内存长度是当前环境下内存中最下寻址单元 1字的长度(通常是4字节对应32位),而指针指向的类型如果是一个实际的对象(不是指向指针),实际对象的内存占用可能并不是1字长度。
一个要知道的概念是, 编译器不是仅访问指针中的那1字长度的数据记录的地址,而是通过1字的数据找到那片内存,从那1字的位置开始,按照指针所指向的类型所占用的内存长度访问对应内存长度的数据 。
可以结合我们前面提到的指针移动操作来进行理解, 指针移动其实就将指针的地址移动到了原位置n个指针指向类型后/前的变量的地址
---------2.9.3.4 指针与数组
数组是可以 按照一定的数组元素类型进行分割的,一片连续的内存
而 数组名 的那个变量, 多数情况下等效于指向数组首元素的一个指针
我们通过 arry[n] 访问数组第n个下标的变量时,其实 arry[n] 被展开为 *(arry+n)
由于arry的指向类型就是数组的元素类型,*(arry+n)的就通过一个指针移动再进行*操作访问
这也是为什么数组下标要从0开始的原因,因为 arry[0]-->*(arry+0)-->*arry 而arry是指向首元素的指针
数组有堆数组和栈数组之分
事实上 堆数组才是真正的“数组名是指向数组首元素的一个指针”的数组 ,而栈数组,多数情况下数组名可以等效于指向数组首元素的一个指针,但其实 栈数组,数组名其实是那一片栈数组内存的代称
通常在函数传参时,通过数组名访问时(arry[n]),以及给其他变量赋值时,栈数组和堆数组无区别
会产生区别的点在于:
1.栈数组名不能进行指针移动操作
2.栈数组名不能更改指向(因此它常被当作一个常量指针)
从1中的附图也可以看出,栈数组名不可以作为左值,无法更改
3.一维栈数组名指针取地址后转换而来的类型并不是一个普通的二级指针(这种 int (*)[n]的指针无法对下层int[3]进行指定),二维栈数组名本身也不是一个普通的二级指针
4.指向栈数组名的指针没有意义,栈数组名是一片内存的代称,而不能当作一个普通对象
5.extern的声明与定义的问题
就不附图了,这是一个很经典的例子
a.cpp:
char s[]="11111";
b.cpp:
extern char* s
由于栈数组名是一片内存的代称,虽然参数传值时能被正常转换为对应类型的指针
但在这种情况下,则是强制解释数组的前32位为一个指针
6.使用sizeof计算时
---------2.9.3.5 理解传参原理与指针传参的应用
函数的参数传递是在函数执行时在栈中开辟空间,并将外部传入的参数变量其内存中的数据拷贝到参数列表中
还要注意的是如果 传递二维及以上的数组,需要给出在一维以上的长度数据 ,这涉及到内存对齐问题。 如不给出一维以上长度则无法使用arry[i][j]来进行索引(arry[i]的指针移动距离无法确定),而只能使用arry[i*width+j]当作一维数组进行索引(width是列坐标的最大值)
---------2.9.3.6 栈数组的本质
对于高维数组而言,一种是通过 int[x][y][z] 这样定义出的栈数组,这种情况其实是通过对齐量,将内存中一维存储的数据,对齐从而形成了高维数组的效果
我们在传参时必须按照 int[][y][z] 或 int(*)[y][z]传递也是为了保留对齐量
一个经典的测试代码如下:
也是由于对齐量的存在,数组只能进行一次向指针的转化,高维的栈数组并不是一个int***的变量,和传参时一样,必须保留对齐量
对于栈数组的解引,其实是 按照对齐量在一维下跳转到相应的位置
而对于像 int*** 这样形式的高维数组而言,对它进行解引,则是 真的多次进行了寻址
无论是栈数组,还是int***,对 arry[Mx][My][Mz] 进行的访问 arry[1][2][3] 都可以被展开为
*(*(*(arry+1)+2)+3),特别的仅当arry是栈数组时,可以等效于 arry[1*My*Mz+2*Mz+3]
这里我们用一段代码来解释有关栈数组的本质,栈数组与指针的区别,以及测试你是否还记得这一问题
#include<stdio.h>
int main() {
int arr[4][5][6]={0};
arr[1][2][3]=5;
int*p=arr[1][2];
int*p2[3];
int **p3[2];
p2[2]=p;
p3[1]=p2;
printf("%d\n",p3[1][2][3]);
printf("%d\n",*(*(*(arr+1)+2)+3));
printf("%d\n",*(*(*(p3+1)+2)+3));
printf("%d\n",*((int*)arr+1*5*6+2*6+3));
printf("%d\n",*((int*)p3+1*4*5+2*5+3)); //唯一错误的访问
------2.9.4 指针的引用
引用不是一个对象,因此没有引用的指针
但指针是一个对象,因此有指针的引用
------2.9.5 复合对象的定义
要注意的是复合对象在定义时 声明符仅作用于第一个定义出的复合对象
一下两种写法没有对错之分,坚持其中一种都会是一个好的习惯
---------2.9.5.1 定义解析法
可以通过 链式解释 来解析复合对象的定义,这在加入了下面要提到的Const后也是一样
要注意的是结合的优先级 从变量名开始 括号中的 > []=() > * > 基本数据类型
------2.9.6 内存分区概述
前面提到了一些内存分区的概念,这里对内存分区做一个简单的概述
内存主要有五大分区:
1.静态区/数据区
2.BSS区
3.代码区
4.栈区
5.堆区
1.存放程序中已初始化的全局变量,静态变量,字符串常量,可分为只读段和读写数据段,字符串常量一般放在只读段
2.存放显式初始化的全局变量,静态变量。我们有提到全局变量会默认初始化(非显式初始化)进行内存清0,而显然没必要把每个0都储存起来。BSS段储存的是那些清0的内存首末地址,以便执行清0操作和之后的修改。BSS区只占用实际运行时的内存空间,而不会在程序二进制文件中
3.用于储存我们所写的代码,函数,代码中的字面值常量。多为只读,但有些架构会允许修改
4.由编译器自行管理的栈区,在需要时会被自行开辟(定义局部变量,执行一个函数时),不需要时会被回收。栈区不产生内存碎片效应
5.由程序员手动开辟并管理的堆区,使用malloc new时就是在堆区开辟空间。堆区又称动态区,在程序运行中被动态的开辟和回收。短时间内重复大量的开辟会导致产生过多的内存碎片(储存原理,内存空间必须以2的整指数倍的内存长度储存,不够的会自行补齐),导致堆区的空间不足
---2.10 Const关键字
------2.10.1 Const的作用
Const修饰关键字用于 限制某一层级的数据不可被修改,或者说给变量的某个数据层级加上只读属性,以提升安全性
------2.10.2 Const常量
如果在编译时就可以确定一个某个添加了基本数据类型的Const的值,那么编译器会对所有出现该Const变量的地方进行替换
而如果编译时值无法确定,则Const关键字会令其数据无法在初始化后被修改。且 Const基本数据类型必须被初始化
如果需要在多处引用同一个Const常量可以:
1.在一处定义extern Const,多处extern Const进行声明,运用extern的全工程查找链接
2.在头文件中extern Const 对应源文件中定义,多处引用头文件
------2.10.3 Const层级
对于基本数据类型Const修饰符会令其在创建后无法被修改
对于复合对象Const关键字的作用可能是对复合变量某个层级的数据执行只读限制
我们引入Const层级的概念,一个 变量本身所对应的那片内存中的数据处于最高层级 ,而通过 复合对象去访问(*操作),被访问到的内存数据处于低层级 。 基本数据类型只有一个最高层级,而复合对象根据其修饰符产生的类型,有多个层级
顶层Const变量必须被初始化,而底(下)层Const可不初始化,未被Const层级的值可修改,被Const的层级无法修改
赋值时:
1. 等号左侧出现的被赋值层级不能为Const,但顶层Const的初始化赋值是例外
2. 左侧被赋值的变量,与右侧用于赋值的变量相比,下层的Const属性只多不少起码相同 ,否则无法完成类型转换(否则可能通过被赋值的变量修改到原变量Const的数据) 指针类型赋值的一种例外,Const属性少的可以被转换为多的
3.注意赋值时, 一般对象之间的赋值顶层部分只是对数值进行了拷贝,产生复合链接关系的是底(下)层,在赋值顶层是产生了两个不同的变量。因此无所谓赋值顶层的Const对比,反正在顶层会产生两个不同的变量。而对于引用来说,引用的顶层是引用的绑定,本就不可变,通过引用来改变绑定对象则变为了下层,或者说引用类型的绑定并没有创建出变量,使用引用类型其实是调用了其绑定的原有变量,因此引用类型需要进行顶层Const对比,左侧进行绑定的引用类型包括顶层Const和下层Const相比右侧只多不少起码相同
4.顶层Const仅在初始化时被赋值,且必须完成初始化,转换关系同2
结合顶层拷贝,底(下)层产生链接,以及这样的转换关系对于赋值时的限制。真正保证了Const变量自身不被修改,且不从外界被获取后进行修改
------2.10.4 Const与引用
Const引用称之为“对常量的引用”,或简称为“常量引用”
用Const修饰限定的引用, 仍可获取到值,但不可通过该引用对值进行修改
Const引用在初始化时,允许用任意字面值表达式变量来进行初始化,只要其值能够转换为该类型的数据即可,不必和引用类型对应 , 引用初始化的一种例外
其实本质是在需要转换时编译器创建了转换后的中间常量并绑定了上去(反正改不了)
经常结合传参和返回值使用,利用引用传递的高效,同时确保程序的安全性
------2.10.5 Const与指针
前面有提到Const与复合对象的关系,主要也是用指针进行讲解
对于指针Const会修饰给其某一层的数据添加只读属性
注意拷贝原理以及链接和转换关系,以正确的理解和使用Const指针
---2.11 Constexpr
其实C++中的Const相当于C#中的readonly
而C++中的Constexpr相当于C#中的Const
假装自己已经解释完了...
前面已经提到过被定义为顶层Const的一类变量,如果编译期就能够确定它的值,那么后面使用到它的地方就会被替换为它的值。而如果不能确定就会添加只读属性
关键字Constexpr相当于 必须在编译期确定值 并进行替换的一类Const,用Constexpr定义变量也被叫做 常量表达式
允许定义 Constexpr类型返回值的函数 ,这类函数必须能够在编译期运行就能获得返回值
由于需要在编译期确定值,因此由Constexpr定义的变量 必须初始化 ,初始化可以使用字面值,或者使用Constexpr类型返回值的函数
能够作为字面值的类型包括直接写在代码中的基本数据类型,字符/字符串,NULL/nullptr,无法写出自定义类型,IO,string类型的字面值
Constexpr类型的指针初始值必须是nullptr或0或者其他储存在固定地址中的对象的地址(比如全局变量),在函数内部直接定义的变量被存放在栈中,它们的初值不固定,但函数内部也可以定义一些超出函数本身作用域的变量(static)
Constexpr定义的复合对象,只读属性一定作用于顶层
---2.12 类型处理
我们可以通过一些关键字让编译器对类型进行一定的处理,定义类型别名,推导类型,以方便变量类型在定义变量时的使用
------2.12.1 类型别名
我们可以使用 typedef 关键字为类型定义别名以便更好的使用变量类型进行变量的定义
typedef int Number;
//typedef 原类型 类型别名;
C11新标准还规定了使用 using 来定义类型别名
我们通常使用能够 暗示变量使用目的 的类型别名
使用typedef可以一次性定义多个类型别名使用','间隔,定义类型别名时 * &只跟随原类型不跟随别名
我们可以通过别名作为原类型来定义别名
类型别名与本名等价,出现本名的地方就可以使用类型别名
------2.12.1.1 复合对象的类型别名
我们可以 定义复合类型的别名 , 一些复合类型的数组需要通过类型别名才能完成定义
#include <iostream>
#include <stdlib.h>
using namespace std;
void(*g_TYPEDF_VoidFunPVoid)();
typedef decltype(g_TYPEDF_VoidFunPVoid) VoidFunVoid;
void fun1() {
cout << "Funtion 1" << endl;
void fun2() {
cout << "Funtion 2" << endl;
int main() {
VoidFunVoid p[10] = {0};
p[0] = fun1;
p[1] = fun2;
for (int i = 0; i < 10; i++) {
if (p[i]) {
p[i]();
cout << "----Test Over----" <<endl;
system("pause");
return 0;
如果使用复合对象的类型别名进行定义,在配合其他关键字(Const)或声明符列表可能会产生特殊的效果(反解问题)
当使用类型别名并结合Const 复合对象声明符列表进行定义时,并 不能通过替换类型别名为本名并通过之前提到的链式解析来进行理解
外部的Const 声明符列表会 作用于类型别名的顶层
------2.12.2 编译器推导类型(auto)
前面提到了通过auto进行定义,并进行初始化,让编译器根据初始化所使用的变量或表达式来推导类型
要注意auto关键字并没有改变C/C++作为动态语言的本质,程序运行前编译器的类型推导已经完成,变量的类型已经确定
使用auto的多重声明,初始基本类型必须一致 (除复合对象声明符列表外的类型不许相同,const可导致不同)
auto的推导会忽略顶层const,但保留底层const ,原因是由于数值赋值时顶层仅是值的传递,生成了不同的变量,而下层会产生链接关系,因此和Const类型的赋值一样不比较顶层Const
使用auto定义 顶层const 变量,我们就需要 显示声明 出来
使用引用类型做初值时, auto推导出的是引用类型绑定的变量的类型 (并导致引用类型自己的const设定无效,根据引用类型绑定的变量的Const进行推导), 如果需要auto推导出引用类型也需要显示声明
当 显示声明推导出的类型为引用类型时,赋值所使用的变量其顶层Const会被保留 (由于引用是绑定到了原有的变量上)
------2.12.4 编译器推导类型(decltype)
decltype() 填入变量或表达式,编译器分析变量/表达式得到类型而不进行计算此时表达式可为定义出的变量赋值
decltype(funtion()) f; 并没有调用函数funtion而是根据其返回类型定义了变量f
可由decltype定义变量,让其类型与某个变量一致,或与表达式一致
decltype在进行类型推导时, 会保留顶层底层const以及引用属性,引用不作为同义词的例外
一个 +0的小技巧 ,引用会使用绑定的变量参与表达式,最终类型就是引用绑定的类型
注意指针*号操作获得的是解引后的引用类型,而不是原类型
通过 添加括号来保证一定推导出引用类型
decltype((i)) 与 decltype(i) 区别在于前者一定推导出引用类型,就算i自身并不是引用类型,而后者只在i是引用类型时才推导出引用类型
---2.13 自定义数据结构(结构体)
这里本书还没有讲到类,或者说和类还有相当一段距离,我们先学习C语言中的结构体这种自定义数据类型
下面定义出一个结构体
struct Student_data{
int id;
char name[16];
float score;
结构体的存在是为让我们将基本的数据类型进行组合,从而定义出自己需要的一种数据类型用以表现程序中某种 数据集 的抽象
事实上即便是C++、甚至是C#这样的面向对象的语言中,我们依然会 使用结构体作为数据储存类型 ,从而实现 数据集和类功能的解耦
结构体的定义是创建了一种 自定义的数据类型 ,因而你可以 使用结构体类型创建 出该结构体的 变量/实例
Student_data st1;//栈中创建了一个类型为Student_data的变量
Student_data* st2=(Student_data*)malloc(sizeof(Student_data));//堆中创建了一个Student_data的变量并获取了指针
事实上你可以在定义结构体的同时在后面紧跟变量名,同步定义变量(但通常不建议在中大型程序中这么做,练习/测试偶尔写写)
struct Student_data{
int id;
char name[16];
float score;
}st1,arry_st[10];//定义了一个st1的变量和一个arry_st的数组
结构体变量如果定义在栈中那么将不被初始化,定义在全局则会被内存清0
C11新规定类/结构体允许对字段设定初始值来替代默认初始化(但这只对栈变量有用)
struct Student_data
int id=0;
char name[16]={0};
float score=0;
你也可以在定义时通过花括号引起的初始化列表来引启初始化
Student st1{ 0,{0},0 };
结构体变量使用‘.’号访问,或是通过 '->' 完成对于结构体指针解引再使用‘.’号访问的操作
Student_data st1;
Student_data* st2 = (Student_data*)malloc(sizeof(Student_data));
cout << st1.id << endl;
cout << st2->id << endl;
---2.14 预处理器
预处理阶段是在正式开始编译前,运行一些预处理操作来部分改变我们的代码
宏定义的展开,const常量展开,#include展开,都是在预处理阶段
Primer在这里提到了 头文件保护符 的定义和使用
头文件保护符是为了 防止 我们的 头文件被重复的引用,多次替换展开 到一个cpp文件中
通过#define定义一个预处理变量,#ifdef该预处理变量是否已定义直到#endif为止
这样仅在第一次未检查到预处理变量的定义时替换从#ifdef到#endif之间头文件的部分,从而防止重复包含
#define Test_H
#ifdef Test_H
//书写头文件
#endif
预处理变量名应 全大写 ,并且保证每个头文件都有独有的变量名,通常以下划线链接并按一定的格式书写
在书写头文件时加上头文件保护符是一种书写头文件的规范
不过像VS这样的编译器还提供另一种头文件保护符的写法,只需要在头文件的开头加上 #program once 即可,并且预处理速度比#define更高
第三章
在第三章我们将学习C++标准库为我们提供的 string 可变长字符串类型, vector 可变长集合,以及 迭代器 的相关知识
建立在我们已经掌握了变量的基础知识之上,本章将教我们如何更好的储存,管理,操作变量
在C语言中我们在编写程序是面临的一大问题就是元素集的可变长问题,C语言虽然在C99引入了string,但对于可变长集合/容器,往往需要我们手动实现
C++的标准库则为我们提供了功能强大的vector可变长顺序容器,并引入了作为容器和遍历算法中介的迭代器
第二章讲到的 基本变量直接实现于计算机硬件,数组的实现也依赖于硬件,体现了大多数计算机硬件的基本功能
而本章讲到的这些类型 尚未直接实现于计算机硬件 ,但相比基本数据类型,拥有 更加灵活,强大的功能
3.1 使用using
事实上在1.2.4我们就已经提到了使用using namespace引用命名空间以及命名空间冲突的问题
引用命名空间使得我们不需要在使用 :: 来查找命名空间中的定义
Primer中在这里提到了使用using引用命名空间的单个定义,这使得我们之后可以直接调用
using std::cin;
//之后就可以直接调用cin了
其实 大多数情况下我们都会一次性引用这个命名空间 ,而不是逐个定义引用
但逐个引用能有效的 防止命名污染
并且Primer在这里提到了 一个重要的原则:不要在头文件中引用命名空间 ,仅在cpp中引用命名空间,从而 防止命名污染
3.2 string字符串
string是C++提供的可变长度的字符串类型
C语言中字符串其实是一个char[] (也被称作C风格的字符串),C风格的字符串在使用中存在着诸多问题,为此C++中使用string来提供字符串的相关功能
string提供了 c_str() 方法可以获取到C风格的字符串,返回值是一个const char*
--- 3.2.1 引入string的命名空间
引入string的头文件和命名
事实上正式的C11的标准库都头都不带有.h
带有.h的库其中的定义都在全局空间中,而C11的标准库定义都在命名空间std中
//Prinmer中提到引用<string>即命名
#include<string> //<string.h>其实是C语言中的一个扩展库包含处理C风格字符串的一些全局方法 例如strcmp
using namespace std::string;
//但我们一般直接引用整个std命名空间
注意必须包含<string>头文件 ,io中的xstring与我们这里所讲的string有所不同(没有对io的<< >>运算符进行重载)但它们拥有相同的名称
C++标准库中兼容了从C语言中继承来的一些头文件,但例如C语言中的<name.h>头文件C++中将其命名为cname(去掉.h后缀前加c)
我们在编写C++程序时也应保证使用cxxx命名的C语言头文件,以区分那些是从C语言中继承而来的,那些是C++独有的
头文件中的内容与C语言尽可能保持一致,但定义的 变量/函数 都被加入进了std命名空间,以符合C++的标准,标准库中定义的命名都应被包含在在std命名空间中
---3.2.2 string的初始化
string的初始化,默认初始化(没有任何字符),拷贝字符串初始化( 不拷贝字符串最后的空字符 ),(int,char)初始化
string s; //默认初始化 空的字符串
string s0("string");
string s1="string";
//拷贝初始化
string s1 = string temp("string");
temp是中间变量
string s2=s1;
//拷贝初始化
string s3(10,'s');
//直接初始化 s3是10个's'字符组成的字符串
------3.2.2.1 直接初始化与拷贝初始化
这里就先简单提一下直接初始化和拷贝初始化的概念,我们将在之后具体学习C++中的自定义类时,再做展开讲解
直接初始化即使用构造函数进行初始化 ,自定义类默认初始化会调用无参数列表的构造函数
拷贝初始化则比较复杂,可能来自变量的隐式转换,赋值拷贝,表达式运算结果的隐式转换
拷贝初始化可能存在中间变量的问题,当初始化所使用的变量或表达式能够通过隐式转换转换为对应的类型时,就会产生一个转换后中间变量,再由中间变量通过拷贝构造函数对实例进行初始化
拷贝初始化将调用拷贝构造方法,默认的拷贝构造方法是将类型中的所有字段进行赋值
拷贝初始化存在深拷贝和浅拷贝的区别,针对指针这种类型的拷贝,是直接传递地址值(浅拷贝),还是将指针所指的对象构造出一个新的,用新对象的地址完成拷贝(深拷贝)
编译器的优化策略,能不产生中间变量就尽量不产生,拷贝初始化--->直接初始化
string s1="string";
//上面提到会产生中间变量的拷贝构造
//编译器可能会优化为
//string s1("string"); 从而避免中间变量的产生
---3.2.3 string的操作
string对象上的操作体现了 C++的类设计习惯 ,通过重载运算符,索引器,定义get/set方法,定义操作方法,来实现对类实例的数据进行方便的读写,类功能的实现
对字符串的<,<=,>,>=比较操作如果字符串A比字符串B更长,那么认为A>B为真,如果等长就是第一对相异字符的Ascii码大小比较结果
两个string可以相加得到和字符串,string也可以与“”引启的常量字符串相加或是与‘’引启的常量字符相加,这是由于string重载了传入常量字符串/常量字符的构造函数,但这也要求 表达式中必须有string对象参与加和(常量字符(串)本身不能相加和) ,否则结果无法给string对象辅助/初始化
---3.2.4 控制台中string的I/O操作
读入string对象,通过cin输入流进行读取(string重载了>>运算符),忽略空格和换行符
输出string对象,通过cin输入流进行读取(string重载了>>运算符)
使用gerline读取整行,保留空白,直到遇到换行符(不包含换行符)
通过while循环读取,检测文件流结尾(包含换行符)
whilie(cin>>string); //前面有提到cin会返回流状态,遇到文件结尾就会终止
getline+while循环读取
whilie(getline(cin,string));//这里getline也会返回流状态,遇到文件接位时终止
自定义结尾符的读取
empty和size方法
string.empty();//返回布尔值判断字符串是否为空
string.size();//返回字符串的短字符长度
string,clear();//清空字符串
注意 一个中文占两个短字符长度 (一个中文是需要占两个短字符长度的 宽字符 )
string::size_type类型
定义在类内部的类型
无符号整形,区别于int,能容纳string最大长度的数值
注意string::sizetype是无符号类型(接近于UINT),因此前面有提到针对无符号类型的恒非负问题(type _ type<0 恒为true),可能导致错误的死循环
C11中string::size_type可以被auto推导出
注意无符号类型在运算中的隐式转换问题
---3.2.5 处理String对象
我们经常需要遍历string中的每个字符,进行逐操作处理
表3.3中列出了一些在cctype头文件中定义的处理单个字符的方法
我们可以使用 范围for循环 (C11新特性),来遍历string/数组/Vector中的每个对象
for(auto c : str){
//对c进行逐字符操作
//通常结合auto推导类型使用
//范围for循环会对冒号后面的对象进行逐访问操作
如果我们需要修改string/数组/Vector,就需要使用 引用类型的循环变量 进行访问,非引用类型的循环变量访问是对拷贝出的值进行的访问,无法进行修改
对于部分访问,可通过一般for循环+下标索引访问(同数组遍历),也可以通过迭代器完成,使用下标时要注意下标的越界问题,就string而言注意使用 str.size()获取大小来限制下标
---3.3 Vector
Vector是一种 线性列表容器 ,可以存储大多数内置类型对象和类类型对象,但不能保存引用类型(引用类型不是对象),可以保存Vector作为存储类型从而构成二维/高纬线性表
C++中倾向于使用Vector替代数组进行对象的管理
------3.3.1 Vector的定义与声明
当我们需要使用Vector时,需要包含头文件并引用命名
#include <vector> //头文件
using std::vector; //引用命名
声明Vector
vector<int> ivec;//在栈区创建
vector<int> *vec_Int = new vector<int>();//在堆区创建并通过指针接收
上面实例化的Vector是用于储存int整形类型的Vector
由<>引启的是 泛型参数列表
Vector是一种 模板类(泛型类) ,需要提供相应的类型参数列表来指定其具体实例化为何种类型, Vector需要提供一个泛型参数用于指定其保存的对象类型
C++中对于泛型编程,及提供泛型函数(模板函数),也提供泛型类(模板类),后面会详细讲到有关C++泛型使用的相关知识
Vector可以存储Vector作为元素从而构成高维线性表,二维int线性表的定义如下,其中早期编译器要求必须在定义时加入一个空格
vector<vector<int>> block;//二维线性表
vector<vector<int> > block2;//早期编译器必须加空格
------3.3.2 初始化Vector
我们可以不必刻意的去初始化Vector,只给出Vecotor的定义,让其默认初始化为一个空的容器,在之后的编码中再动态的增删元素,Vector支持高效的进行动态增删元素,因而这的确是一种编码的思路
但也有以下几种方法,可以在需要的时候,对Vector进行初始化
拷贝初始化
可以通过一个已有的Vector进行 拷贝初始化,但要注意储存类型相同
vector<int> ivec;
for (int i = 0; i < 10; i++) {
ivec.push_back(i);
vector<int> ivec2 = ivec;//拷贝初始化
列表初始化
可以使用花括号,字面值参数列表,进行列表初始化
vector<string> strVec{ "Hello","World" };//初始化,包含两个值为“Hello”和“World”的string
列表中的字面值,对于值类型来说是赋初值
对于类类型列表的参数,相当于是
string str = "str";//这样进行了初始化
但这里其实是通过公开的单参数构造函数隐式转换产生中间实例,再用中间实例逐元素进行拷贝初始化,最后销毁中间实例
但如果我们将列表中填入对应类型的实例, 依然会通过该实例生成中间实例,再用中间实例拷贝初始化Vector中的元素 ,并不是直接拷贝
总之底层逻辑是统一的:
先想办法用列表中的参数创建出中间实例,用中间实例拷贝构造,最后销毁中间实例
创建指定元素数量的初始化
圆括号引起一个int值定义容量,不设初始,内置类型将进行内存清0
类类型调用默认构造函数(无参数列表),如果没有公开的默认构造函数无法进行值初始化
vector<int>arry(10);//10个
vector<int>arry(m);//m个
在此基础上,可以提供一个参数用于初始化所有元素,这个参数为内置值类型赋值,或作为类类型公开的,有唯一一个参数的构造函数的输入
vector<int>arry(10,0);//10个值都为1
vector<int>arry(m,n);//m个值都为n
vector<string>arry(5,"Hello World");//5个值都是Hello World
初始化高维Vector的方法
vector<vector<int>> arry(m, vector<int>(n, 0));//m*n二维Vector,m和n都可以是变量
vector<vector<vector<int>>> arry(x, vector<vector<int>>(y,vector<int>(z)));//三维
注意区分!!!
圆括号中的值可以理解为 用于构造Vector ,指定数量,以及一个统一的赋值/构造函数传参
而花括号中的值, 则是针对Vector所存储的变量 ,进行拷贝构造
某些情况下,花括号中填入了一个int值,但int无法被隐式转换为Vector存储的类型,或是填入了一个int值和另一个类型的值,这个值可以隐式转换为Vector存储的类型, 那么编译器会尝试替换为圆括号的创建指定元素数量的初始化情况
------3.3.3 向Vector中添加元素
Vecotr可以进行高效的动态添加,最基本的方法是push_back();
像链表一样,将新元素添加到最后方
Vector能进行高效动态添加/扩容,但最好不要默认初始化,而是使用值初始化预测一个容,这涉及Vector的动态扩容问题,后面会提到
指定位置的添加/删除则需要通过 迭代器 结合insert/erase来实现
------3.3.4 其他Vector操作
可通过范围for循环访问Vector
循环访问时应保证下标的增减运作无误,如果我们在循环中改变了Vector的长度,就要对下标做出调整
如果需要在循环中动态改变Vector长度,就不能使用范围for循环,因为下标不在我们的控制之中, 范围for循环中不应改变访问容器的长度
empty size方法功能同string
可以进行索引,从而通过一般for循环访问(通过size限制下标范围)
size的类型是Vector中定义的size_type无符号类型
>,<关系运算符同string,字典顺序比较
当长度和存储所有对象均相等时,两个vector相等,否则若对应位置的对象均相等,长度短的小于长度大的
如果相同,则是第一组不同元素的比较
只有存储类型可通过关系运算符比较时才能使用相应的关系运算符比较Vector ,类类型需要重载关系运算符,使相应的Vector可比,并定义比较时的具体判定逻辑
---3.4 迭代器
迭代器是标准库容器的一种功能,所有的标准库容器都可以使用迭代器,但只有少数几种支持下标索引,因而C++也更倾向于使用迭代器对容器中的元素进行访问
之所以要有迭代器,是由于部分标准库/STL容器并不是线性表的形式,例如树/图,当我们仍希望能够依次访问所有元素时就需要使用迭代器
string对象并不是容器,但它支持许多与容器类似的操作,例如支持迭代器,同时支持下标索引
迭代器与指针类似提供了间接访问对象的方式,当迭代器指向某个元素或尾元素下一位置时称之为有效迭代器,其它指向称为无效迭代器
与指针不同,迭代器的获取使用begin end成员函数返回,而不取地址符
vector<int> v;
for (int i = 0; i < 10; i++) { //向Vecotr中依次添加0~99
v.push_back(i);
auto b = v.begin();//指向第一个元素
auto e = --v.end();//指向最后一个元素
其中end返回的是指向尾元素下一位置的迭代器,称之为 尾后迭代器/尾迭代器 ,实际这个位置上并不存在元素
如果容器为空begin/end都返回尾后迭代器
---3.4.1 迭代器操作
所有的迭代器都支持通过 ++/-- 自增自减来指向之前/之后的元素,并通过 ==/!= 来进行比较
注意end()返回的尾后迭代器不能对其递增或解引用
注意不要让迭代器超越尾迭代器,否则程序在运行时出错
注意使用!=判断,而非<,并不是所有容器的迭代器都支持<,但一定支持!=
迭代器的类型是定义在对应的类中的,并且分为一般迭代器和const只读迭代器
vector<int> v;
for (int i = 0; i < 10; i++) { //向Vecotr中依次添加0~99
v.push_back(i);
vector<int>::iterator b = v.begin(); //vector<int>::iterator 一般迭代器
vector<int>::const_iterator cb = b;//vector<int>::const_iterator 只读迭代器
通常两种迭代器均可用,由begin()/end()返回的是一般迭代器(可读可写)
如果string/vector被定义为const常量,那么就只能使用const迭代器,begin/end返回const迭代器
我们可以通过cbegin/cend,无论容器是否为常量,都固定获取const迭代器
对于设计上只读的操作最好使用const迭代器进行访问
迭代器类似于指向元素的指针,可以通过 * 操作解引访问元素
箭头运算符(->)与指针的效果类似,可直接访问元素的成员
---3.4.2 迭代器失效
动态数组容器
对于Vector这种动态数组性质的容器,事实上我们在添加/插入元素时, 并不是每次添加/插入都会导致数组的内存扩容 ,vector会在插入元素突破内存临界时, 一次性扩容一段较大的内存长度(2倍或者1.5倍) ,将临界推移到一个更大的界限
没有导致扩容,那么仅仅是插入位置之后的迭代器失效(push_back/emplace_back末端插入不导致失效)
如果本次插入导致突破了内存临界,导致扩容,那么所有迭代器都会失效
但对于Vector, 删除时,并不会回收之前分配的内存(clear亦不会),但删除会导致删除位置之后的元素重新排列,进而
指向被删除元素,及其位置后的任何元素的迭代器都将失效
非数组容器
对于list这种链表形式的容器,或是像map,tree这种树形结构的容器,它们的元素 内存并不是连续分配的,而是各自为立被结合到一起
对非数组容器添加/插入不会导致迭代器失效
在删除时, 只会使指向删除位置的迭代器失效
运用erase/insert 返回值
事实上我们 不应该去利用/或者说应该少用,迭代器在插入/删除元素后不失效的特性 ,尤其是在遍历并修改容器的操作时
我们应该 多使用erase,insert的返回值 ,事实上它们会返回完成删除/插入操作后,对应操作位置的有效迭代器(或者信息)
vector或者list的
erase 会为我们返回完成 指向删除位置之后的有效迭代器 ,
insert则会返回 指向插入位置,或者插入区间头部的迭代器
而map set 的insert则会返回一个pair值, pair<set<T>::iterator,bool>
包含插入是否成功(唯一性),以及对应成功插入位置/失败重复位置元素的迭代器
---3.4.3 string&vector迭代器运算
所有容器的迭代器都支持之前提到的自增/减,通过==和!=进行比较
此外string和vector的迭代器提供了额外的运算操作
迭代器相减获得的距离是定义在vector和string中的 different_type带符号整数类型
使用迭代器运算时,注意不要让迭代器超越尾迭代器,否则程序在运行时出错
使用迭代运算进行二分查找,注意我们要一直累加.begin() 因为相减/2之后结果是different_type整数类型,并不是一个迭代器
书中给出了一段基于迭代器经典的二分搜索算法:
------3.4.4 迭代器初始化
对于Vecotr或是list这类的 顺序容器 ,我们可以通过圆括号引起的两个迭代器参数来进行初始化,将顺序拷贝两迭代器之间的元素
vector<int> v;
for (int i = 0; i < 24; i++) {
v.push_back(i);
vector<int>v2(v.begin(), v.end());
//拷贝 [v.begin(),v.end()) 之间的元素
vector<int>v2(迭代器m, 迭代器n);
mn指向的元素类型必须与Vector保存的元素类型相同 ,Vector会 通过地址[m,n)之间的元素进行拷贝初始化 ,过程中会 不断自增移动m直到n,因此n必须大于m
借助一定的偏移量运算,我们可以使用特定的部分来初始化Vector
---3.5 数组
C语言时代就有的一种管理对象的方式
与Vector不同,数组必须在定义时确定其容量,后续容量是固定不可修改的
前面在C语言部分我们已经较为详细的讲到了数组,数组与指针的关系,数组传值等内容,这里就不再赘述重复内容了
数组的定义/初始化,栈数组必须通过整数常量/字面值指明数组的大小,或通过列表初始化来间接给出数组大小, 特别的字符数组可以通过常量字符串初始化并设置大小 ,C/C++均不支持使用变量来设置栈数组大小
int arry[M];//M必须是常量/字面值
int arry2[5] = {0,1,2,3,4};
char str[] = "Hello World";//通过字面值拷贝初始化,间接指定大小
char str2[5] = {'a','b','c','d','e'};
堆数组可以通过变量指定一维大小(使用malloc或new int[]),但其余各维对齐量必须是常量
int m = 2,n=3;
auto arry = new int[m];//正确
auto arry = new int[x][y][z];//x可以是变量,yz必须是常量或者字面值
数组不允许进行拷贝,但部分编译器可能支持这一非标准操作的扩展
数组可以通过下标索引元素进行访问,其索引下标的类型是定义在cstddef.h中的 size_t无符号类型 ,但我们通常以int整形索引,数组下标是否越界需要程序员负则检查,进行越界访问时可能造成无法预计的不确定后果
不知道长度的情况下,只有栈数组可以通过范围for循环访问 (以int[3] int(*)[2][3]的对齐量来限制范围),堆数组不能通过范围for循环访问(int*范围不确定)
高维栈数组使用范围for循环,除内层外,其它层必须使用索引访问(否则成为了int*指针而不是 int(*)[2][3]这样有对齐量的类型)
int arry[24];
int arry2[2][3][4];
int* p = (int*)arry2;
for (int i = 0; i < 24; i++) {
arry[i] = i;
p[i] = i;
//范围for循环访问一维栈数组
for (auto i : arry) {
cout << i << " ";
cout << endl;
//高维栈数组只能在解引到 int[] 后可以用范围for循环访问
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
for (auto k : arry2[i][j]) {
cout << k << " ";