打算好好学学 C++,一方面是因为 C++ 和视觉结合的相关代码是很多的,另一方面它和底层硬件也有很强的联系,因此打算深入学学底层基础性的语言,最好能够达到自如书写代码的目标。
主要学习途径是买了《C++ Primer Plus》,也是调查了一些领域内的书买下来的,很厚共有 18 章,但是内容很详实很细致,非常适合学习。
书里面的例程一个个在电脑里复现一遍,这样实际操作感觉学起来进步很快,并且能发现一些光看看不出来的知识点。
但是还存在的问题是这个过程中的个人体会、想要记忆的点都是散乱的分布在书里的每一页,时间长了很容易忘。遂开一篇文章专门进行记录,方便以后回来进行查阅,也更有利于学习提升。
第一、二章:入门及函数
起始流程
第一章主要是一些预备知识,我并不了解一个 c++ 程序应该如何编译、调试,因此着重看了这些内容。
Linux 下的编译主要使用 g++ 进行编译,使用指令如下:
1 | g++ mycode.cxx |
windows 下如果要在命令行同样是使用上述命令,编译成功会生成可执行文件。
此前在 Linux 下对 c++ 进行编译时使用了 Cmake,具体怎么个原理还不太懂,之后专门去研究研究。
当然 Windows 下一般都是用其特有的编译器去进行编译要更方便,这便引出了 Microsoft Visual C++。软件提供了很多类型的 IDE,在创建项目前需明确。
软件中,Build 和 Make
表示编译所有源代码文件中的代码,Link
表示将编译后的源代码与库代码进行组合,Run
是运行程序,如果没有执行编译等步骤,Run
会在运行前先完成这些,另外 Debug
表示以步进方式进行调试。
函数练习及基本语句
C++ 中是不能省略分号的,每个语句都是以分号作为结束标志,而不是以换行作为标志。
一个函数 int main()
开头 int
规定的是函数返回值类型,而括号中规定函数接受的参数,为空表示不接受任何参数。
1 |
|
头文件、名称空间
iostream
相当于是定义一些“流”的头文件,及“in/out stream”,使用 cin
和 cout
进行输入输出的程序必须包含 iostream
。
旧式 C++ 及 C 使用的头文件均为 .h 文件,新式 C++ 不带任何拓展名,不带任何拓展名的头文件在使用时需要规定名称空间 namespace
。
关于名称空间,我现有的理解是有两个人名是相同的,都叫 Chandler,你在叫其中一个人的时候就无法准确叫到。这时候名称空间相当于规定一个“姓氏”,一个叫 Chandler Bing,另一个叫 Chandler John,这样就可以准确叫到你要找的人。
1 | using namespace std; |
上面语句引入名称空间,相当于声明后面的相关函数都使用这个名称空间下的,放到例子里面就是你声明你之后喊得 Chandler 都是姓 Bing 的,如果不声明,后面要使用某个函数时就要带上名称空间,如 cout 就变成了:std::endl
。
消息显示与传递
前面提到的 cin
和 cout
分别代表输入输出,注意这个输入输出时说外部程序到代码的输入输出,cin >> val
表示将用户输入的某个值传给变量 val,cout << "hello world!"
则表示将这句话输出到程序中显示。
而这里的 <<
和 >>
都是插入运算符,输入和输出时流式传输的,是一个完整的信息流,与其说这两个符号让信息显示或储存,倒不如说他们只是在输入和输出流中“插入”了一段信息,这样的表述更加确切。
endl表示换行,使用传统的“\n”也可,看喜好。
声明语句和变量
c++ 中所使用的任何变量都需要进行声明,如 project 的声明语句可以为:int project;
,这一规则的引入主要是为了防止错误拼写变量名。另外,c++ 中可以连续使用赋值运算符,且运算符从右往左进行。
cout 指令可以使程序输出打印字符串这点功能是和 printf()相当的,但是 printf()更笨一点,需要声明函数的各个类型。
类简介
类的使用是语言面向对象编程的核心概念之一,之前在学习 python 的时候就有点迷惑这个概念要表达的意思,现在慢慢向着基础方向挪动,才逐渐理解了一些。
类定义的是某一类别的整体概念,比如它可能会定义程序员有哪些功能,可以写 C++ 代码、 python 代码等等。而对象就具体化到了实体,比如程序员 XXX 就是一个实体,他有类所规定的功能,你也可以让他去写 c++ 代码,也可以让他写 python 代码,这就具体了起来。你可以某个程序员做一件事,但不可以让程序员这个概念本身去做事,这也就是类的实例化过程。
函数补充
函数需要有函数原型,就像变量需要有变量声明,函数原型就是将函数输入及返回值作出定义。
名称空间的声明可以放在函数外面,让所有函数都能够使用这一名称空间中的所有元素,也可以放在函数里面,让某个特定函数才能使用。
第三章:c++基本类型
基本类型、复合类型合称为 c++ 的数据类型,基本类型有整型、浮点型,复合类型则包括数组、字符串、指针、结构以及相关的一些变体,数据类型是一切的起点,也是最贴合底层逻辑的所在,需要非常非常清晰的掌握。
整型
类型、初始化、声明
整形包括 short、int、long、long long 以及 char,各自有各自的无符号类型。
简单的变量初始化有三点,类型、名称以及值,如:int var = 1;
,这其中 int var;
可以单独写开,他是前面提到的变量声明。但一般情况下需要在变量声明的同时进行赋值,防止出现忘记赋值的情况。
无符号类型的声明只需要在声明前再加上 unsigned 即可,如:unsigned int var;
,如果整形变量发生溢出,就会在另一端开始取值,在使用较大的数据时,需要注意选择适当数据类型。
字面值、进制
整型字面值是显式的书写常量,此处介绍十进制 dec(decimal)、十六进制 hex(hexadecimal)、八进制 oct(octal)以及二进制 bin(binary)。
第一位为 0 ,第二位为 1-7 时,是八进制表示,此时基数为 8;前两位为 0x 或 0X,则基数为十六(十六进制),其中 A-F 表示十六进制位(小写同样);十进制则是正常的书写规范。示例如下:
1 | int chest = 42;//表示十进制42 |
char 类型
char 类型是用来处理单字符(字母或数字)的类型,在程序及存储中,这些字符是以 ASCII 值的形式存储的,只有在输出到 cout 或从 cin 输入时会根据类型进行转换,这也是为什么 char 类型属于整型的一类。
单字符在书写时需要用单引号引起,而不能像字符串一样用双引号。
另外还有一种 bool 类型属于布尔变量,字面值用 true 或者 false 表示。
const 限定符
const 限定符的作用就是放在声明变量前,通过限定声明来确定常量,不同于变量声明,常量声明过程中必须有初始化,并且后面值不能被改变。
浮点数
在计算机进行表示时,浮点数分为两部分,一部分将浮点数表示为同一形式,另一部分用于对值进行放大或缩小,及缩放因子。缩放因子的作用就是移动小数点的位置,这也是浮点数名称的由来。
浮点数表示方法有两种,一种是标准小数点表示法,及正常情况下的小数点表示法;另一种则是 E 表示法,格式为: 5.98e24
、8.33E-3
,e、E 皆可,后面跟着指数位,同样正负皆可,表示的是 10 的几次方。
浮点数有三种类型,float、double 和 long double,区别在于它们可以表示的有效位数,注意有效位数不依赖于小数点位置。
一般情况下,默认类型是 double 类型,希望常量为 float 类型则使用 f 或 F 后缀,对于 long double 类型使用 L 后缀。
算术运算符
算术运算符包括 + 、— 、* 、/ 、%,加减乘除不用多描述,% 表示取余。
int 类型的计算中仅保留整数位,小数部分直接舍弃,并非四舍五入,因此可以和取余进行搭配使用。
强制类型转化通用表达格式为 (typeName) value
或 typeName (value)
,如:
1 | (long) thorn |
小结
这些基本类型的存在有其特定的意义,在我看来,刚开始只需要明白存在的类型进行选择即可,等到实际引用时再去根据数据范围限制找对应的类型进行更改,这样的学习效率会高很多。
第四章:c++ 复合类型
复合类型中,数组可以存储同种类型的多个值(或者存储字符串)。结构可以存储不同类型的不同值。指针则是将数据地址告诉计算机的变量。他们之间互有关联,甚至在某些情况下可以互相代替,了解这一点是最重要的。
指针相关操作直接影响系统内存,这给了操作者极大权限的同时也产生了不小的变成隐患,同时这也是 c++ 编程中不容忽视的一大问题,因此需要对内存这部分进行深度理解。
数组
基本定义
数组声明指定三点信息,元素类型、数组名、元素数,声明数组通用格式如下:
1 | int months[12]; |
声明中所有的值需要已知,即元素数不可以变化,需要事先确定好。
数组从 0 开始编号,因此数组中最后一个元素的索引比数组长度小 1。
初始化(初始化=声明+赋值)
只有在定义数组时可以进行初始化,并且不能将一个数组值赋给另一个数组,也可以之后一个元素一个元素的进行赋值。
1 | int cards[4] = {3,6,8,10}; |
字符串
字符串可以按 char 数组的方式进行定义(及上方数组初始化方法进行定义),也可以使用双引号字符串常量进行初始化。需要注意空字符也占一个元素位,在声明元素数量时必须考虑在内。
1 | char cat[4] = {'c','a','t','\0'}; |
cin 使用空白(空格、制表符、换行符)确定字符串结束位置。需要读取一句话时,往往会有空格出现,这是需要引入新的函数:getline()
或 get()
。getline()
遇到换行符时停止读取(即回车),并且不保存换行符到字符串中。但 get()
不会丢弃换行符,而是留在输入队列中,不带参数的情况下,get()
则可以读取下一个字符。因此两种函数通常的使用如下:
1 | cin.getline(name,arrSize); |
string 类简介
string 类的导入需要直接引入 #include<string>
,引入后可以将字符串视为和 int 等类型差不多的类型,以初始化为例:
1 | string str = "string"; |
包括不同字符串之间的赋值、相加,这些字符串数组无法完成的功能,字符串类都可以像 int 等类型一般直接使用 = 、+ 完成。
结构
定义
创建结构有两步:首先定义结构模板,其次按模板描述定义结构变量。
1 | struct str_name |
上述是结构模板创建的例子,规定结构内有哪些类型,以及对应的名字。str_name
是这种结构类型的名字,下面为结构变量声明:
1 | str_name myname; |
这里的 myname
就是结构,myname.name
或 myname.volume
就是此结构下的结构变量。
结构模板声明需要放到所有函数外部,结构声明可以根据适用范围自行选择。
结构数组
要创建 100 个 str_name 结构的数组,可以:
1 | str_name gifts[100]; |
这样数组中的每一个元素都是 str_name 结构的对象,可以与成员运算符一起使用。
共用体
共用体声明示例如下:
1 | union one4all |
pail 有时可以是 int 变量,有时又是 double 变量,但它一次只能存储一个值,为的就是节省空间,相当于简洁版 struct。
指针
指针变量和存储数据的变量是一枚硬币的两面,指针指出数据地址,根据数据地址可以获取数据值;数据变量存放着数据值,根据相关运算符同样可以获得数据值的存放地址。使用常规变量时,值是指定的量,地址是派生量;使用指针变量时,地址是指定的量,值是派生的量。
概念
如果 home
是一个变量,那么 &home
就是它的地址。
指针存在的概念是为了使得内存能够动态分配,常规情况下定义某个数组,它的大小、存储空间都是确定的,运行过程中无论它有没有赋值,它所占的内存永远在。但指针存在的目的使得数组能够动态的去调整,实际运行时需要多少就给多少,并且能在使用结束后删去这一部分内存。这种策略是指针的核心。
指针也是变量,它存储值的地址。*运算符被称为间接值或解除引用运算符,应用于指针可以获得指针地址处所存储的值。
若 mainly
是一个表示地址的指针变量,则 *mainly
和常规变量等效,这就可以用声明常规变量的方式去声明 *mainly
,而 *mainly
的声明就相当于声明了指针变量 mainly
。
1 | int var; |
每个指针变量的声明都需要一个 *,如 int* p1,* p2
。
可以在声明语句中初始化指针,这种情况下初始化的是指针,而不是其指向的值:
1 | int * pt = &var; |
一定要在使用解除引用计算 * 之前,将指针初始化为一个确定且适当的地址。
分配(new)与释放(delete)内存
指针真正的运用在于,在运行阶段分配未命名的内存以存储值,分配内存使用 new
运算符,其通用格式及示例如下所示:
1 | typeName * pointer_name = new typeName; |
new
分配的内存块通常与常规变量声明分配的内存块不同,变量的值都存储在成为“栈”的内存区域中,而 new
从被称为“堆”或者“自由存储区”的内存区域分配内存。
new
对内存进行分配后,在使用结束时需要进行内存释放,不释放则可能会引发内存泄漏等严重后果,因此需要配对使用 new
和 delete
。
使用 delete
时,后面要加上指向内存块的指针,如:
1 | int * ps = new int; |
注意,不要尝试释放已经释放的内存,也不要使用 delete
来释放声明变量所获得的内存(不在同一内存区),其只能用于释放 new
分配的内存。
动态数组的创建
事先为数组分配内存被称为静态联编。但在使用 new
时,如果运行时需要数组,则创建空间;不需要,则不创建。这样创建数组的过程叫做动态联编。
动态数组的创建与释放:
1 | int * pt = new int[10]; |
方括号告诉程序需要删除的是整个数组,而不是指针指向的元素。(指针指向的是数组第一个元素的地址)
若指针指向的是第一个元素的地址,那应该如何访问其它元素?只需要将指针当作数组名使用即可,如 pt[1]
、pt[2]
等。
在很多情况下,指针和数组基本是等价的(除了数组名不能修改,指针值能修改这一点)。
指针与数组
c++ 将数组名解释为地址,数组第一个元素的地址,数组指针也同样指向第一个元素地址。
若指针变量加 1,其实际值是增加其对应类型的占用字节数,如指针指向 short 类型,而 short 占用两个字节,将指针加 1 时,指针的值将加 2。
大多数情况下可以用相同的方式使用指针名和数组名,两者的区别之一在于指针值可修改,数组名不可修改;之二在于对数组应用 sizeof 运算符得到的时数组长度,对指针使用则获得的是指针长度。
指针概念性要点:指针声明、赋值与解除引用,指针算数,数组动态联编与静态联编,数组表示法与指针表示法。
指针与字符串
在 c++ 大多数情况下,char 数组名、char 指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。
指针如果被声明为 const,则编译器将禁止改变 bird 指向的位置中的内容。
不可以使用字符串常量或者未被初始化的指针来接受输入,一个是因为常量无法被修改,另一个是因为不知道应该存储在哪里,都会引起错误。
一般来说,如果给 cout 提供一个指针,它将打印地址。但如果指针的类型是 char,则 cout 将显示指向的字符串。如果需要显示地址,则要对指针类型进行强制转换,如 int 。(注意此处的 int*
是规定的指针类型,规定指针指向 int,而不是像 int
,因为地址本身并不是整型数据。
要将字符串复制到数组中需要用到 strlen()
、strcpy(ps,str)
。strlen()
可以获得字符串的长度,strcpy(ps,str)
第一个参数是目标地址,第二个参数是要复制的字符串的地址。
再改进一点的话还有strncpy(ps,str,19)
,增加了第三个参数限制长度,防止由于错误赋值导致最后一位不是空字符的情况发生。
动态结构的创建
将 new 用于创建动态结构由两个步骤组成,创建结构和访问其成员。
创建的步骤比较简单,如下所示:
1 | my_struct * ps = new my_struct; |
比较困难的地方在于访问成员,创建结构时,不能将成员运算符句点用于结构名,因为只有地址,没有名称。
对此提出了专门的运算符:箭头成员运算符(->)。例如,如果指针 ps 指向一个 my_struct 结构,则 ps->name 是被指向结构的 name 成员。
还有另一种方法,如果指针 ps 指向一个 my_struct 结构,则 ps 就是被指向的值,也就是结构本身。因此 (\ ps).name 本身就是该结构的 name 成员,c++ 的运算符优先规则要求使用括号。
自动存储、静态存储和动态存储
函数内部定义的常规变量使用自动存储空间,它们在函数被调用时自动产生,在该函数结束时消亡,通常存储在栈中。
静态存储则是整个程序执行期间都存在的存储方式,使变量成为静态有两种方式:一种是定义在函数外面;另一种是在声明变量时使用关键字 static。
动态存储则时 new 和 delete 操纵的内存池,被称为自由存储空间或堆。内存池用于静态变量和自动变量的内存时分开的,数据的生命周期不完全受程序或函数的生存时间控制。
数组、vector、array
模板类 vector 和 array 是数组的替代品,vector 声明形式如下:
1 |
|
vector 对象可以在插入或者添加值时自动调整长度,下面的声明创建一个名为 vt 的 vector 对象,可以存储 n_elem 个类型为 typeName 的元素:
1 | vector<typeName> vt(n_elem); |
其中 n_elem 可以为整型常量,也可以为整型变量,vector 就相当于动态数组,功能比数组更加强大,但是效率稍低。
array 类长度固定,效率与数组相同,但是更加方便安全。其创建示例如下:
1 |
|
与 vector 不同的点就在于 n_elem 不能是变量。
综上,vector 将 new、delete 的部分写成模板可以直接应用,简化了创建动态数组的过程;array 与数组则好似 string 与字符串数组,赋值、运算等功能比数组要更容易实现。
结语
整体来看,数据类型这两章的概念十分贴合计算机运行的底层逻辑,也可以感觉出 c++ 相对于 C 作出的许多改进(string、vector、array)。但是学的过程仍旧从数组基本类型入手,在日后处理错误时会更加明了,也可以更加了解这许多类的运行机制原理,毕竟之后还需要对 stl 进行学习。指针存在的意义我认为就是增加运行效率,节省运行过程中不必要的内存,这也是日后各种高级算法需要实现的核心目标。