目录
一、结构&类
在C语言中的结构(一个学生,把学号、名字、性别、成绩等成员变量放到一起,构成结构),也叫结构体,但在C++中,不叫结构,而叫“类”。(类,只在C++中才有的概念,C语言中并没有类的概念。)
在 C++ 中, 结构体 (struct) 可以包含函数(即方法), 而且还支持继承和多态等面向对象编程特性。但在 C 语言中,结构体只能包含数据成员。
在结构和类中,有三个重要的权限修饰符,分别是public(公有)、private(私有)、protected(保护),C语言中不涉及权限修饰符。
C++中的结构和类的区别:
①C++中结构体内部成员变量及成员函数默认的访问级别是public,而C++中类的内部成员变量及成员函数的默认访问级别是private。
②C++中结构体的继承默认是public,而C++中类的继承默认是private。
二、基于对象&面向对象
1. 基于对象的程序设计
这种把功能包到类中,定义一个类对象并通过该对象调用各种成员函数实现各种功能的程序书写方式,称为基于对象的程序设计。
2. 基于对象和面向对象程序设计的主要区别
基于对象和面向对象程序设计的主要区别是:在基于对象的程序设计中额外运用了继承性和多态性技术,从而变成了面向对象程序设计。
3. 面向对象的程序设计的优点
(1)易维护:派生类会有不同的接口,那每个接口的维护修改都在自己的类中进行。
(2)易扩展:通过继承性和多态性,可以少写很多代码,实现很多变化。
(3)模块化:通过设置各种访问级别来限制别人的访问,保护了数据的安全。
三、C++结构
1. 源文件后缀名
不同的C++编译器会使用不同的文件后缀名,如有.c、.cpp、.cc、.cxx这些源文件后缀,.cc、.cxx一般在GNU编译器上比较常见。此外,还有.m、.mm——如果在MacOS苹果电脑上用Xcode进行开发,它们用的是Objective-C语言,但里面有时也会嵌入C或者C++代码。一般.m就暗示代码含有Objective-C和C语言语句,.mm就暗示代码含有Objective-C和C++语句。
2. 头文件扩展名
C++语言的头文件扩展名一般以.h居多,此外,还有.hpp。.hpp一般来讲就是把定义和实现都包含在一个文件里,有一些公共开源库就是这样做,主要是能有效地减少编译次数
很多C++提供的头文件已经不带扩展名。以往在C语言中经常用到的头文件,如比较熟悉的stdio.h等,在C++98标准之后,都变成了以c开头并去掉扩展名的文件,如stdio.h就变成了cstdio文件,但是cstdio和stdio.h是两个文件,用#include命令表示的时候只需要写成#include<cstdio>,至于cstdio里面做的事,肯定包含stdio.h中做的事,而且额外还做了一些其他事。
3. 编译型语言
C++本身是属于编译型语言。什么叫编译型语言呢?程序在执行之前需要一个专门的编译过程,把程序编译成二进制文件(可执行文件),执行的时候,不需要重新翻译,直接使用编译的结果就行了。
相对于编译型语言,还有解释型语言。解释型语言编写的程序不进行预先编译,以文本方式存储程序代码。但是,在执行程序的时候,解释型语言必须先解释再执行。显然,编译型语言执行速度快,因为它不需要解释。而像Lua等语言,就属于解释型语言。
4. 命名空间
命名空间就是为了防止名字冲突而引入的一种机制。系统中可以定义多个命名空间,每个命名空间都有自己的名字,不可以同名。可以把命名空间看成一个作用域,这个命名空间里定义的函数与另外一个命名空间里定义的函数,即便同名,也互不影响(因为命名空间名不同)。
命名空间定义可以不连续,可以写在不同的位置,甚至写在不同的源文件中。如果以往没有定义该命名空间,那么这就相当于定义了一个命名空间,如果以往已经定义了该命名空间,那这就相当于打开已经存在的命名空间并为其添加内容。
四、C++输入输出
1. C++中输入/输出用的标准库
C语言中,通常往屏幕上输出一条信息会用到printf函数,但在C++中,通常不用printf进行输出,而是用C++提供的标准库。
C++中输入/输出用的标准库是iostream库(输入/输出流)。什么叫流?流就是一个字符序列。
std::endl是一个函数模板名。std::endl一般都在语句的末尾,有两个作用:
- 输出换行符\n。
- 刷新输出缓冲区,调用flush(理解成函数)强制输出缓冲区中所有数据(也叫刷新输出流,目的就是显示到屏幕),然后把缓冲区中数据清除。
2. 输出缓冲区
可以理解成一段内存,使用std::cout输出的时候实际上是往输出缓冲区中输出内容。那么输出缓冲区什么时候把内容输出到屏幕上呢?有如下几种情况:
- 缓冲区满了。
- 程序执行到main函数中的return,要正常结束了。
- 使用std::endl了,因为使用后会调用flush()。
- 系统不太忙的时候,会查看缓冲区内容,发现新内容就正常输出。所以有时使用std::cout时,语句行末尾是否增加std::endl都能将信息正常且立即输出到屏幕。
五、局部变量初始化
在C语言中,如果某个函数中需要用到一些局部变量,那么局部变量都会集中定义在函数开头,而在C++中不必遵循这样的规则,随时用随时定义即可。
1. 传统编码方式中,可以使用“=”在定义变量的时候进行初始化。
int a = 3.5f; //a=3
上面这行代码是可以编译通过的,但执行起来后会发现,实际上a因为是int类型,所以3.5的小数部分会被截断,结果是a的值等于3 。
2. 在C++新标准中,可以使用“{}”在定义变量的时候进行初始化,需要额外说明的是,在“{}”之前还可以增加一个“=”号。
int b{ 3.5f }; //编译不过
int c = { 3.5f }; //编译不过
上面两行代码根本无法编译通过,直接报语法错,这样做的好处是不会使数据被误截断,进一步保证所写的代码的健壮性。
3. 用“()”也可以对变量进行初始化。
int d(3.5f); //d=3
六、auto关键字
auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。
七、头文件防卫式声明
重复#include的问题时有发生,无法避免,那么如何解决这个问题呢?这就要从.h头文件本身入手,通过使用#ifndef、#define、#endif解决这个问题。
通过使用#ifndef、#define、#endif的组合,避免了.h头文件中的内容被多次#include。
要习惯性地在文件头部增加#ifndef、#define语句行,在文件末尾增加#endif语句行。出现在.h头文件中的这三行代码,被习惯性地称为“头文件防卫式声明”。
八、引用与常量
1.引用是为变量起的另外一个名字(别名),一般用“&”符号表示。之后,该引用和原变量名代表的变量看成是同一个变量。一般来说:定义引用并不额外占用内存。或者也可以理解成,引用和原变量占用的是同一块内存。定义引用类型的时候必须进行初始化,不然给哪个变量起别名呢?
2.constexpr关键字这是C++11引入的关键字,也代表一个常量的概念,意思是在编译的时候求值,所以能够提升运行时的性能。编译阶段就知道值也会带来其他好处,例如更利于做一些系统优化工作等。
constexpr int Fun(int i)
{
return ++i;
}
void Test()
{
constexpr int var1 = 1;
constexpr int var2 = var1 + Fun(var1); //函数调用在常量表达式中必须具有常量值,所以Fun也得定义成constexpr
std::cout << "var1:" << var1 << ",var2:" << var2 << std::endl; //var1:1,var2:3 var1不变
}
上述代码中,因为var2是常量,初始化时调用了Fun函数,所以Fun也得定义成constexpr。
3. 引用参数和指针参数相比,引用参数更安全。
九、范围for、new内存动态分配与nullptr
1. for遍历
std::vector<int> vec = {1, 2, 3};
for(auto& item : vec) {
item *= 2;
}
在C++中,遍历容器时是否使用引用取决于我们的需求:
- 如果需要修改容器中的元素,则应该使用非常量引用;
- 如果不需要修改容器中的元素,但元素是大型对象或者复制成本高昂时,使用常量引用可以避免不必要的拷贝;
- 如果容器中存储着基础类型(如int、char等)或者小型结构,并且并不打算改变它们,则直接按值访问就足够了。
2. new内存动态分配
在C语言内存中供用户使用的存储空间包括程序代码区、静态存储区、动态存储区。程序执行所需的数据都放在静态存储区和动态存储区。例如,全局变量放在静态存储区,局部变量放在动态存储区。
在C++中,把内存进一步更详细地分成5个区域:
- 栈:函数内的局部变量一般在这里创建,由编译器自动分配和释放。
- 堆:由程序员使用malloc/new申请,free/delete释放。malloc/new申请并使用完毕后要及时free/delete以节省系统资源,防止资源耗尽导致程序崩溃。如果程序员忘记free/delete,程序结束时会由操作系统回收这些内存。
- 全局/静态存储区:全局变量和静态变量放这里,程序结束时释放。
- 常量存储区:存放常量,不允许修改,如用双引号包含起来的字符串。
- 程序代码区:相当于C语言中的程序代码区。
(1)堆和栈的区别
堆和栈都相当于C语言部分所说的动态存储区,但用途不同。下面总结一下堆和栈的区别:
- 栈空间有限(这是系统规定的),使用便捷。例如代码行inta=4;,系统就自动分配了一个4字节给变量a使用。分配速度快,程序员控制不了它的分配和释放。
- 堆空间是程序员自由决定所分配的内存大小,大小理论上只要不超出实际拥有的物理内存即可,分配速度相对较慢,可以随时用new/malloc分配、free/delete释放,非常灵活。
(2)malloc/free与new/delete
在C语言(不是C++)中,malloc和free是系统提供的函数,成对使用,用于从堆(堆空间)中分配和释放内存。malloc的全称是memoryallocation,翻译成中文含义是“动态内存分配”。
注意两点:
-
配对使用,有malloc成功必有free,有new成功必有delete。
-
free/delete不要重复调用,因为free/delete的内存可能被系统立即回收后再利用,再free/delete一次很可能把不是自己的空间释放掉了,导致程序运行出现异常甚至崩溃。
(3)malloc/free与new/delete的区别
new不但分配内存,还会额外做一些初始化工作,而delete不但释放内存,还会额外做一些清理工作。这就是new/delete这一对比malloc/free这一对多做的事情。所以,在C++中,不要再使用malloc/free,而是使用new/delete。
3. nullptr
nullptr是C++11引入的新关键字,代表“空指针”。
在函数重载时,因为NULL和nullptr类型不同,所以如果把这两者当函数实参传递到函数中去,则会导致因为实参类型不同而调用不同的重载函数。
- 对于指针的初始化,能用nullptr的全部用nullptr。
- 以往用到的与指针有关的NULL的场合,能用nullptr取代的全部用nullptr取代。
十、函数
1. 函数声明
(1)前置返回类型
int Test1(int, int);
上面这种写法叫作“前置返回类型”,也就是说函数的返回类型位于函数声明或者函数定义语句的开头。
(2)后置返回类型
在C++11中还引入了一种新的语法,叫后置返回类型,也就是在函数声明或定义中把返回类型写在参数列表之后。
auto Test1(int, int)->int;
总结一下“后置返回类型”的写法:前面放置auto关键字,表示函数返回类型放到参数列表之后,而放在参数列表之后的返回类型是通过“->”开始的。
2. 内联函数
函数定义之前增加了一个inline关键字,增加了这个关键字的函数,叫作内联函数。
调用函数是要消耗系统资源的,尤其是一些函数体很小但却频繁调用的函数,调用起来很不划算,因为要频繁地进行压栈、出栈动作以处理函数调用和返回的问题,这也意味着要频繁地为它们开辟内存。为了解决这种函数体很小、调用又很频繁的函数所耗费的系统性能问题,引入了inline关键字。
(1)inline关键字的效果
- 影响编译器,在编译阶段完成对inline函数的处理,系统尝试将调用该函数的动作替换为函数的本体(不再进行函数调用)。通过这种方式,来提升程序执行性能。
- inline关键字只是程序员(开发者)对编译器的一个建议,编译器可以尝试去做,也可以不去做,这取决于编译器的诊断功能,也就是说决定权在编译器,无法人为去控制。
- 传统书写函数时一般将函数声明放在一个头文件中,将函数定义放在一个.cpp源文件中,如果要把函数定义放在头文件中,那么超过1个.cpp源文件要包含这个头文件,系统会报错,但是内联函数恰恰相反,内联函数的定义就放在头文件中,这样需要用到这个内联函数的.cpp源文件都能通过#include来包含这个内联函数的源代码,以便找到这个函数的本体(源代码)并尝试将对该函数的调用替换为函数体内的语句。
(2)使用内联函数的优缺点
用函数本体取代函数调用,显然可以增加效率。但同时带来的问题是函数代码膨胀了。所以内联函数函数体要尽可能短小,这样引入inline才有意义。
(3)其他补充
- 可以把constexpr函数看成是更严格的一种内联函数,因为constexpr自带inline属性。
- 内联函数有点像宏展开(#define),宏展开和内联函数有各种差别,如类型检查等。
3. 函数调用
(1)函数返回类型为void表示函数不返回任何类型。但是,可以调用一个返回类型为void的函数让它作为另一个返回类型为void的函数的返回值。
(2)有时从函数中返回内容时,系统会临时构造一些必需的东西并做一些并不为人熟知的操作来实现return目的。
(3)C++中,更习惯使用引用类型的形参来取代指针类型的形参,所以提倡读者在C++中多使用引用类型形参。
4. const形参
(1)函数重载
下面这对重载的函数不可以,因为const关键字在比较同名函数时会被忽略掉。这两个函数相当于参数类型和数量完全相同,因此函数重载不成立,编译链接时会报错。
int Test1(int i) {
}
int Test1(const int i) {
}
(2)如果不希望在函数中修改形参里的值,建议形参最好使用常量引用。
(3)把形参写成const形式的习惯有许多好处:
- 可以防止无意中修改了形参值导致实参值被无意中修改掉。
- 实参类型可以更加灵活,可以接收普通引用作为实参,也可以接收常量引用作为实参。
void Test_2(int& i)
{
}
void Test_3(const int& i)
{
}
int main()
{
int i = 10;
int& num = i; //正确
const int& num1 = i; //正确
//int& num2 = 10; //错误
const int& num3 = 10;//正确
//Test_2(125); //错误
Test_3(125); //正确
}
(4)常量指针: char const*p;等价于const char *p;不能修改*p。
(5)指针常量:char* const p,不能修改p。
十一、string&Vector
在C语言中,一般会用字符数组来表示字符串。在C++中,依然可以用字符数组来表示字符串,也可以用string类型来表示字符串,而且,字符数组和string类型之间还可以相互转换。
1. c_str()
返回一个字符串中的内容指针(也就是说这个内容实际上就是string字符串里的内容),返回的是一个指向正规C字符串的常量指针,所以是以“\0”结尾的。这个函数是为了与C语言兼容,在C语言中没有string类型,所以得通过string类对象的成员函数c_str把string对象转换成C中的字符串样式。
std::string str = "hello world";
const char*c = str.c_str();
2. 字面值和string相加
(1)两个字面值不能直接相加。
(2)下方代码可以理解成"123"+s结果肯定是生成一个临时的string对象,然后又跟789相加,再生成临时对象,然后复制给str。
//std::string str = "123" + "456"; //错误
std::string s = "456";
std::string str = "123" +s+ "789"; //正确
3. vector
一般来讲,vector容器里面可以装很多种不同类型的数据作为其元素(容器中装的内容简称“元素”)。但是,vector不能用来装引用。请记住,引用只是一个别名,不是一个对象。
std::vector<int*> list;
//std::vector<int&> list2; //错误
(1)vector初始化
1. 创建指定数量的元素。请注意,有元素数量概念的初始化,用的都是“()”。
2. 如果不给元素初值,那么元素的初值要根据元素类型而定,例如元素类型为int,系统给的初值就是0,元素类型为string,系统给的初值就是"",但也存在有些类型,必须给初值,否则就会报错。
3. 一般来说,“()”一般表示对象中元素数量这种概念,“{}”一般表示元素的内容这种概念(并不绝对)。
std::vector<int> list(5,1); //五个整型,全是1
std::vector<int> list1{ 1,2,3 }; //三个整型,分别是1,2,3
std::vector<int> list2(5); //五个整型,全是0
std::vector<std::string> list3(5); //五个string,全是""
(2)遍历vector
在for语句中,不要改变vector的容量,增加、删除元素都不可以。
十二、迭代器
许多容器如vector,在C++标准库中,还有其他容器如list、map等都属于比较常用的容器,C++标准库为每个这些容器都定义了对应的一种迭代器类型,有很多容器不支持“[]”操作,但容器都支持迭代器操作。写C++程序时,强烈建议读者不要用下标访问容器中的元素,而是用迭代器来访问容器中的元素。
在操作迭代器的过程中(使用了迭代器的这种循环体),千万不要改变对象的容量,也就是不要增加或者删除容器中的元素。如果在一个使用了迭代器的循环中插入元素到容器,那只插入一个元素后就应该立刻跳出循环体,不能再继续用这些迭代器操作容器。
1. 迭代器iterator
iterator是每个容器(如vector)里面都定义了的一个成员(类型名),这个名字是固定的。在理解的时候,就把整个vector<int>::iterator理解成一种类型,这种类型就专门应用于迭代器,当用这个类型定义一个变量的时候,这个变量就是一个迭代器。
(1)begin() & end()
每一种容器,如vector,都定义了一个叫begin的成员函数和一个叫end的成员函数。这两个成员函数正好用来返回迭代器类型。
- begin返回一个迭代器类型(就理解成返回一个迭代器)。
- end返回一个迭代器类型(就理解成返回一个迭代器)。end返回的迭代器并不指向容器vector中的任何元素,它起到实际上是一个标志(岗哨)作用,如果迭代器从容器的begin位置开始不断往后游走,也就是不断遍历容器中的元素,那么如果有一个时刻,iter走到了end位置,那就表示已经遍历完了容器中的所有元素。
- 如果容器为空,则begin返回的迭代器和end返回的迭代器相同。
(2)rbegin() & rend()
如果想从后面往前遍历一个容器,那么,用反向迭代器就比较方便。反向迭代器使用的是rbegin成员函数和rend成员函数。
- rbegin返回一个反向迭代器类型,指向容器的最后一个元素。
- rend返回一个反向迭代器类型,指向容器的第一个元素的前面位置。
(3)迭代器运算符
- *iter:返回迭代器iter所指向元素的引用。必须要保证该迭代器指向的是有效的容器元素,不能指向end,因为end是末端元素后面的位置,也就是说,end已经指向了一个不存在元素。
- ++iter:和iter++是同样的功能——让迭代器指向容器中的下一个元素。但是已经指向end的迭代器,不能再++,否则运行时报错。
- --iter:和iter--是同样的功能——让迭代器指向容器中的前一个元素。
- 迭代器之间可以相减表示两个迭代器之间的距离,迭代器加一个数字表示跳过多少个元素。
2. 迭代器const_iterator
另外一种迭代器类型,叫作const_iterator,从名字上能感觉到其含义:有const在,一般都表示常量,也就是说值不能改变的意思。这里的值不能改变表示该迭代器指向的元素的值不能改变,并不表示该迭代器本身不能改变,该迭代器本身是能改变的,也就是说,该迭代器是可以不断地指向容器中的下一个元素的。
该迭代器只能从容器中读元素,不能通过该迭代器修改容器中的元素。所以说,从感觉上来讲,const_iterator更像一个常量指针,而iterator迭代器是能读能写的。
什么时候用const_iterator呢?如果这个容器对象是一个常量,那么就必须使用const_iterator,否则报错。
(1)cbegin() & cend()
这是C++11引入的两个新函数,与begin、end非常类似。但是,不管容器是否是常量容器,cbegin、cend返回的都是常量迭代器const_iterator
十三、类型转换
C语言风格的强制类型转换——直接把类型用“()”括起来。没有类型方面的检查,直接硬转,转的是对还是错,程序员必须提供保障。除了把类型用“()”括起来之外,其实,如果不括类型,括数字也可以,括数字也称为函数风格的强制类型转换(看起来有点像函数调用)。
在C++中,强制类型转换分为4种:static_cast、dynamic_cast、const_cast和reinterpret_cast。当然,C语言中的强制类型转换依然支持,但这种支持只是为了语言兼容性的考虑。
这4种强制类型转换的名字都以_cast结尾,并且这4种强制类型转换都被称呼为“命名的强制类型转换”(因为它们每一个都有名字而且名字都不同)。
1. 命名的强制类型转换的通用形式
这些命名的强制类型转换的通用形式:
强制类型转化名<type>(express);
其中,强制类型转换名就是static_cast、dynamic_cast、const_cast、reinterpret_cast这4个名字之一,用来指定是哪种转换;type是转换的目标类型;express是要转换的值。
2. 四种强制类型转换
使用reinterpret_cast非常危险,而使用const_cast总是意味着设计缺陷。
(1)static_cast
静态转换。属于编译的时候就会进行类型转换的检查,用的时候要小心,代码中要保证转换的安全性和正确性,与C语言中的强制类型转换的感觉差不多,不要想太复杂了。一般的编译器能够执行的隐式的类型转换也都可以用static_cast来显式完成。
不可用于:一般不能用于指针类型之间的转换,如int *转double *、float *转double*等
(2)dynamic_cast
该转换应用在运行时类型识别和检查(与static_cast不一样,static_cast是编译时类型检查)方面,主要用来进行父类型转成子类型。但是因为要做类型检查,所以检查的代价很昂贵,但也保证了转换的安全性。
(3)const_cast
去除指针或者引用的const属性。换句话说,这个转换能够将const性质转换掉,这个类型转换只能做这件事(功能比较有限)。同样,也属于编译的时候就会进行类型转换的检查。
如果本来是一个常量,若强硬地用const_cast去掉了常量性质并往里面写值,这是一种未定义行为,不要这样做,以免产生无法预料的后果。除非它原来不是常量,后来被变为常量,再后来又用const_cast给它变回非常量,这个时候能往里写值。
const_cast很特殊,只有这个转换能去掉表达式的常量属性,所以这个转换的能力是其他类型转换运算符无法替代的。(const_cast不能像static_cast一样改变表达式类型)。
(4)reinterpret_cast
(不建议使用)也属于编译的时候就会进行类型转换的检查。但这是一个很奇怪的类型转换。reinterpret表示重新解释、重新解读的意思(将所操作的内容解释为另一种不同的类型),用来处理无关类型的转换,也就是两个转换的类型之间没有什么关系,那就等于乱转、瞎转、自由转的意思,就是怎么转都行,所以这个类型转换相当随意。