目录
一、语法规范
二、声明定义与作用域
三、指针专题
四、类型转换
五、常用函数方法
六、输入输出
七、STL使用相关
八、面向对象
九、泛型编程
十、内存分配
十一、编译相关
十二、C++11新特性
十三、待解决问题
附:C语言函数
一、语法规范
- C++中主程序要写成不能写成void main,这是不规范的有些g++编译器会报错,return 0表示已经执行完成了
1
2
3int main(){
return 0;
}
且C++规定:不明确标明返回值的,默认返回值为int,也就是说main()等同于int main(),但会被警告
2.数组元素个数一般用nums.size(),使用sizeof(nums)/sizeof(int)的形式在某些时刻不保险
3.队列的两种实现形式
- 使用循环数组
- 使用链表
4.栈stack:push(), top(), pop(), empty(), size()
队列queue:push(), front(), pop(), empty(), size()
5.头文件引入格式
- 标准库的头文件用<>,编译器会先去存放标准库头文件的路径搜索
- 非标准库的头文件用””(一般是.h文件),编译器会先去main.cpp文件运行的路径搜索
6.int无法表示的大数可以用unsigned long long类型来暂时存放
7.new、delete时C++关键字,malloc、free是C语言关键字
8.int main{}也可以传参
1 | int main(int argc, char *argv[]){ |
例如有一个test.cpp文件代码如下
1 | #include <string> |
则输入./test wo shi a b c(如果只输入./test的话,argc是1,argv[0]=”./test”)
- argc是6
- argv[0] = “./test”
- argv[1] = “wo”
- argv[2] = “shi”
- argv[3] = “a”
- argv[4] = “b”
- argv[5] = “c”
9.由于C++要兼容C的标准库,而C的标准库里碰巧也已经有一个名字叫做“string.h”的头文件,包含一些常用的C字符串处理函数,比如strcmp。但是这个头文件跟C++的string类半点关系也没有,所以<string>并非<string.h>的“升级版本”,他们是毫无关系的两个头文件。<cstring>才是与C标准库的<string.h>相对应,但裹有std名字空间的版本。
10.对空栈进行出栈操作会产生溢出错误
11.引用类型分配在堆中,存的是对某个数据的引用
12.对于i++这样的操作,其实是分3步执行的,读取i的值,增加i的值,回写i的新值。这3步每一步都是原子操作,但是组合在一起就不一定是原子操作了
- ++i比i++效率高得原因是,前者只用返回i+1的结果,而后者既要先保存i的结果还要保存i+1的结果
13.字符串比较大小时,默认规则:先比较字典序大小,再比较长度,即会出现升序排序后,前面str的长度比后面的str长度大的情况
二、声明定义与作用域
1.static关键字的作用
- 全局静态变量(在全局变量前加static):作用域为声明语句所在的整个文件
- 局部静态变量(在局部变量前加static):作用域仍为局部
(静态变量都放在内存的静态存储区内,在程序运行期间一直存在不会释放,所有在函数内定义的局部静态变量,会在第一次运行到该语句时就创建,第二次运行到该定义语句会自动跳过,直到程序终止才释放)
- 静态函数(在函数返回类型前加static):作用域为声明语句所在的整个文件
- 类的静态成员和静态函数:这个类的所有对象都有,注意静态函数只能引用类中的静态成员(静态成员:类和子类的所有方法都可以访问)
总结:static与extern相反,static意味着该变量、函数在被声明的文件之外是不可见的,除非引入声明的文件作为头文件,但是这样的话会每一个引入该头文件的文件都会定义一个自己的变量造成内存浪费,所以一般用extern在头文件中声明才是正确的方式
2.全局声明,局部定义vector实例
1 | vector<int> temple; |
3.当t是自定义数据类型时(包括迭代器、typedef、union、enum),++i效率远大于i++
4.变量的声明与定义
- 变量的声明不分配地址和内存空间,一个变量可以在多个地方声明
- 变量的定义分配地址和内存空间,只能定义一次
5.如果一个变量是用const声明的,那么无论是对它的引用还是指针,前面都要加const,不然会报错
6.开头用constexpr声明的一定是常量表达式,在编译过程就能得到结果而不用等到运行再去计算
7.<iostream.h>这种.h是c的库函数,c++不鼓励继续使用。c中的命名空间是全暴露的,即如果c++中用using namespace std;的话也是一模一样,但为了防止命名冲突、标识符全暴露在全局中等问题,尽量使用
- 如果先重复代码过多,可以开头写using std::cout;来事先声明,后面就可以直接用cout了
- 不推荐直接写using namespace std;,因为这样就抛弃了c++的改进,变得和c一样
8.类型名只能定义一次,即不可以用与全局类型名名字相同的局部类型名
9.定义在函数内部的内置类型变量(int、double等)不会被初始化为0,而是未定义的随机值
10.引用的一些实例
1 | int i = 42; |
11.extern int a 和 int a 区别
- extern int a只是声明a,告诉编译器要到该文件外部去找a的定义,实际这里并未给a分配内存空间
- int a写在全局变量位置就是表明是全局变量的定义,并且分配内存空间,其他文件要使用a时,#include该文件就可以直接在代码里用a了
- 假设文件1中有名为a的全局变量,那么文件2只要include文件1就可以直接用a了,区别在于
- 如果多写一行extern int a,文件2中就不准再写一个名为a的局部变量了
- 不写的话,文件2中可以写一个名为a的局部变量,在文件2中用到a时会优先调用这个局部变量
12.extern “c”{}则告诉编译器里面的代码用c语言编译,c语言编译的特点是c中不支持函数重载
- 可以是声明单条语句
1
extern "C" void box(int, int);
- 可以是声明复合语句
1
2
3
4
5extern "C"
{
double sqrt(double);
int min(int, int);
} - 也可以包含头文件,即表示头文件中所有的声明都是extern “c”的
1
2
3
4extern "C"
{
#include <cmath>
} - 不可以添加在函数内部;一个函数多个声明的话,加在第一个即可
13.枚举类型没赋值的元素会从上一个赋值的元素开始递增
1 | enum etest{ |
14.局部定义的static变量仍是局部变量,只不过局部执行后它不释放而已
1 | int x = 4; |
15.define只执行字符串替换,不进行任何计算
1 | #define A 4+5 |
16.类的static数据成员详解
- 声明,在类中只能声明不能赋值(包括直接=和构造函数赋值都不行),受public、private等影响
1
2public:
static double s_a; - 定义,不能在函数里定义,包括main(),要在全局位置定义
1
2
3double 类名::s_a = 10.0
int main(){...} - 使用,成员访问和类作用域访问都可以
1
2
3
4
5
6
7
8father f1;
child c1;
father *p1 = &f1;
cout << f1.s_a << endl;
cout << p1->s_a << endl;
cout << child::s_a << endl;
cout << father::s_a << endl; - 类外初始化原理:被static声明的类静态数据成员,其实体远在main()函数开始之前就已经在全局数据段中诞生了,此时都还没有类对象,如果是在类内初始化的就会出错
17.类的static函数成员详解(基本和static数据成员一样,下面只列出例外)
- 具体调用的static在哪定义的,就使用哪个类的静态成员(从距离近的开始使用,例如自己的静态成员就比父类的静态同名成员距离近)
- 父类指针调用的是父类的static
18.extern的唯一用法,给加一个定义在.cpp中的全局变量(以下操作等同于直接在.h中int定义全局变量)
- .h中用extern int 声明
- .cpp中用int赋值
- main文件中#include “xx.h”就可以用这个全局变量了
三、指针专题
1.指针和引用的区别
- 指针有自己的一块空间,用sizeof看一个指针大小是4;引用知识一个别名不占空间,用sizeof看引用大小是被引用对象的大小
- 指针可初始化为NULL,引用则必须初始化为一个已有对象
- 作为参数传递时,指针必须被解引用才可以对对象修改,而引用的修改会直接改变被引对象(因此有const指针,却没有const对象)
- 指针可以有多级(**p),引用却只有一级
- 指针和引用使用++运算符的意义不一样
- 返回动态分配的对象、内存必须用指针,因为引用可能引起内存泄漏
2.指针值为NULL和nullptr的区别
- NULL实质上是0,底层表示是(void*)0,所以在int* P = 0时会有问题,不知道这个0指的是数值0还是NULL,而void*类型是不允许隐式转换成其他类型的
- nullptr是C++11推出的,即无论何时都表示空指针
1
2
3
4
5
6ListNode *p1 = 0;
ListNode *p2 = NULL;
ListNode *p3 = nullptr;
ListNode *p4 = p1;
# p1、p2、p3、p4都指向空地址,不过C++更建议使用p3的方法(c++11推出的nullptr)
3.智能指针:实质上是一个类,主要用于管理在堆上分配的内存,将普通的指针封装为一个栈对象。当栈对象生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏
- auto_ptr:采用所有权模式
- unique_ptr:实现独占式拥有,保证同一时间内只有一个智能指针可以指向该对象
- shared_ptr:实现共享式拥有,使用计数机制来表面资源被几个指针共享,每多一个计数加1,计数等于0时资源释放(两个共享指针互相指向对方会造成循环引用,计数失效而导致内存泄漏)
- weak_ptr:用于解决上面循环引用导致的内存泄漏,它不会增加对象的引用计数,可以与shared指针环形转化(注意:不能通过weak指针直接访问对象的方法,要先将其转化成shared指针再访问)
4.野指针:指向一个已删除对象或未申请访问受限内存区域的指针
避免野指针的方法
- 初始化时让指针指向NULL
- 指针被free或delete后让其指向NULL
- 变量作用域结束前释放地址空间并让指针指向NULL
5.常量指针指向一个只读的对象;指针常量表示该指针只在定义时初始化,后面不能让它指向别处
6.四种不同函数指针
1 | # 是一个返回值为void,参数为空的函数指针0 |
7.*类型就是指针,比如int *p、double *q
- 所有指针的sizeof长度相等
- 前面的类型可以决定这个指针的跳跃力,例如p++会跳4个字节,而q++会跳8个字节
- void* 是一个跳跃力未定的指针,所以我们可以在需要时把它转换成任意类型的指针,常用于实现泛型编程的代码中
- 任意类型的指针无需强转可以变成void*指针
- void*指针可以强转变成任意类型的指针
8.&变量表示变量的地址,也是符号给某指针赋值的格式;
1 | int a = 1; |
解引用就是在指针变量前面加&,此时就相当于a
1 | &a_p = 2; // 这条代码作用和a=2一模一样,此时的&a_p就是a,因为a_p指向a的地址 |
9.诸如int **pp = &p是二级指针,它表示pp指向p指针的地址(注意是指向p指针本身的地址这个概念,和p指针指向的地址的不同!),即pp是一个指向指针的指针,p是一个指向值的指针
- 即&p代表的是指针p的地址,而不是代表p指向的地址
- *是指针,**是指向指针的指针,***是指向指针的指针的指针
10.在const定义指针语句中
- 若指针变量名左边先出现数据类型,则表示指针指向的变量的值是不可变的,底层const
- 若指针变量名左边先出现*const,则表示指针是不可变的,顶层const
11.在函数内定义一个用指针指向的量,然后最后返回这个指针时非常危险的,因为在作用域结束后会自动销毁这个对象,此时的指针指向的地址就变成了未定义的,即指针变成了空指针。改善方法:
- 用动态内存new来声明
- 不要这样返回指针
四、类型转换
1.(struct 类型名*)&变量名表示把该变量强转成该类型
1 | # serv_addr是sockaddr_in类型的变量 |
2.C++中的四种类型转换(区别于强转是为了便于追踪错误)
- const_cast:将const变量转为非const,使其能修改,常用于函数重载
- static_cast:用于各自隐式转换,如非const转const、void*转指针等,也适用于大数转小数,告知编译器不在乎精度损失
- dynamic_cast:用于动态类型转换(转指针、引用),只能用于含有虚函数的类,用于类层次间的向上和向下转化
- reinterpret_cast:几乎什么都可以转,比如int转指针,但有可能会出问题
3.将字符串转成整数函数atoi()使用对象是char[]数组,若str是string类型的话应采用如下形式
1 | int a = atoi(str.c_str()); |
还有一种直接将string转数值类型
1 | int i = stoi(str); |
五、常用函数方法
1.memset()用于填充字符串,原型是void *memset(void *str, int c, size_t n),n通常用sizeof(xx)的形式
1 | char str[50]; |
2.在vector中使用find(num.begin(), num.end(), 元素值)
- 找到,返回下标
- 找不到,返回num.end()
3.在字符串中使用find函数
1 | str="ABCDE"; |
- 字符串也可以用find(str.begin(), str.end(), 元素值),这样的话是吧string当成vector
一样的容器,返回值是迭代器类型,搜索值只能是字符,不能像str.find()一样搜索子串返回下标
4.str1.substr(x,n)用于从字符串str1下标x开始截取n个字符串
1 | str1 = "123456"; |
5.四个取整函数
- floor函数会返回一个小于或等于传入参数的最大整数
- ceil会返回一个大于或等于传入参数的最小整数
- fix会返回一个与0和传入参数相距最近的整数
- round会返回一个四舍五入的整数
1
2
3
4
5floor(-10.5) = -11;
floor(2.5) = 2;
ceil(-10.5) = -10;
ceil(2.5) = 3;
6.随机函数rand()的使用方法
- 头文件#include<stdlib.h>
- rand()不需要整数
- rand()会返回一个从0到最大随机数的任意整数
- rand() % 100会返回[0,99]的整数,即rand() % (b-a+1) + a会返回[a,b]之间的任意一个整数
7.初始化随机数种子srand()的使用方法
- 头文件#include <stdlib.h>
- 一般用srand((unsigned)time(0))来保证每次都是不同随机数(time(NULL)也是一样的)
- 如果每次都设相同值,rand()所产生的随机数值每次就会一样
- 如果rand()之前没使用srand(),则默认调用srand(1)
8.计时函数模板
1 | #include <ctime> |
9.print()也有返回值,返回值就是打印的字符长度
10.函数传递参数是通过栈实现的,所以是从右开始往左传
1 | box(printf("a"), printf("b"), printf("c")); |
11.取绝对值函数
1 | i = abs(i); // i是int |
12.字符串常用函数
- 重复构造
1
cout << string(3, '2'); //会输出222
13.将整数转换成二进制表示
1 | #include <bitset> |
13.C++求两个数的最大公约数函数gcd(),原理是辗转相除法,也叫欧几里得算法
1 | int a=2, b=4; |
14.自带的二分查找法
1 | #include<algorithm> |
六、输入输出
1.对已经封装好的exe执行程序使用文件作为cin输入,将cout输出到另一个文件
1 | # 控制台执行 |
2.cout作为参数传递时,类型是std::ostream &,因为原则上cout是一个ostream对象
3.IO库头文件
用于读写流 用于读写文件 用于读写string
4.同一类的对象
- 输入
- cin
- ifstream
- istringstream
- 输出
- cout
- ofstream
- ostringstream
- cerr
5.读写一个IO对象会改变其状态
- 进行IO操作的函数通常以引用方式传递和返回IO对象
- 传递和返回的引用不能是const的
6.缓冲区的刷新
- 默认情况下cin和cerr都是关联到cout的,所以读cin或写cerr都会导致cout的缓冲区被刷新
- flush会刷新缓冲区工作
- endl会换行并刷新缓冲区工作
- ends会输入一个空字符并刷新缓冲区工作
- 使用cout << unitbuf后会设置成每次写操作之后立马进行一次flush刷新缓冲区
- cout << nounitbuf会回到正常缓冲方式
7.文件的输入和输出
- 创建流对象
- ifstream input;
- ofstream output;
- 打开文件并将其与对象绑定
- input.open(“xx1.csv”);
- output.open(“xx2.csv”);
- 上述两步可以写成ifstream input(“xx1.csv”)的形式
- 后面可以加模式参数output.open(“xx2.csv”, ofstream::out | ofstream::app);
- in以读方式打开
- out以写方式打开,默认隐含trunc
- app每次写操作前都自动定位到文件末尾,即保存文件打开前的内容,默认隐含out
- ate打开文件后立即定位到文件末尾
- trunc截断文件(即打开前先清空文件),只有设置out才能设置该模式
- binary以二进制方式进行IO
- 读写文件
1
2
3
4
5
6
7ifstream input("xx1.csv");
string temple;
vector<string> vec;
while(getline(input, temple){ //若是逐个数据输入用while(input >> temple){}
vec.push_back(temple);
}
input.close(); - 判断文件是否处于打开状态input.is_open(); //直接用if(input)也可以判断文件是否open成功
- 关闭文件input.close(); //当创建流对象写在局部时,局部循环结束后会自动销毁流对象,此时会自动调用close(),不用手动写,为保险也可以手动写
8.默认情况下cout和cerr都是输出到屏幕
- cout经过缓冲后输出,默认情况下是显示器可以重新定向
- cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,默认情况下被关联到标准输出流,但它不被缓冲,也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。一般情况下不被重定向
9.读写csv文件
- 一个读csv文件的标准模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15ifstream fin("test1.csv"); //打开文件流操作
string line;
while (getline(fin, line)) //整行读取,换行符“\n”区分,遇到文件尾标志eof终止读取
{
istringstream sin(line); //将整行字符串line读入到字符串流istringstream中
vector<string> fields; //声明一个字符串向量
string field;
while (getline(sin, field, ',')) //将字符串流sin中的字符读入到field字符串中,以逗号为分隔符
{
fields.push_back(field); //将刚刚读取的字符串添加到向量fields中
}
string name = fields[0];
string age = fields[1];
}
fin.close(); - 一个写csv文件的标准模板
1
2
3
4
5
6
7
8ofstream f_out("dataset/cos_matrix_apk.csv");
for(int i = 0; i < 10; i++){
for(int j = 10; j >= 0; j--){
f_out << j << ','; //在csv文件里输出','表示换下一个单元格
}
f_out << endl;
}
f_out.close();
10.解算法题常用输入输出
- 输入
- cin >> a是根据a的类型读入,会根据类型大小、空格、换行符停止
- 若输入123,a是int类型,则a=1,下次再输入时则a=2
- 若输入”123 1”,a是string类型,则a=”123”
- getline(cin, a),会读取一整行到a里面,此处的a必须是string类型
- 注意getline读取的行是当前光标所在的行,若之前用cin读的话,可能光标还在上一行,此时getline获得的就是空
- 解决办法:用cin>>str或者重复调用两次geline即可
- cin >> a是根据a的类型读入,会根据类型大小、空格、换行符停止
- 输出
- cout
七、STL使用相关
1.vector的截取数组
1 | vector<int> nums {1,2,3,4,5,6,7,8,9}; |
2.vector的数组合并
1 | vector<int> nums0 = {1,2,3}; |
3.二维vector的初始化
1 | # 第一种 |
4.使用以下形式赋vector初值
1 | vector<int> hash = vector<int> (1000, 1); |
可以避免产生歧义而编译不通过
5.vector的去除重复元素:先赋给set再传回来
1 | vector<int>nums = {1,1,2}; |
6.哈希表特殊用法
1 | unordered_map <TreeNode*, int> f; |
则有f[root]是一个int值,root是一个树节点指针
7.哈希表可用以下形式来判断键是否在表内存在
1 | memo.count(key) == 0; |
8.对哈希表中存在的键值对的遍历
1 | for(auto& t:map){ |
9.数组用==、!=会比较首内存地址(即是不是指向同一内存),而vector数组就可以用==、!=比较数组中包含的元素是否一样
10.sort()自定排序规则,即lambda
1 | sort(nums.begin(), nums.end(), [](xx a, xxb){ |
11.优先队列priority_queue的用法
1 | #include <queue> |
12.字符串删除末尾字符用str.pop_back(), 删除指定下标字符串用str.erase(index, count);
13.字符串比较大小会自动从第一位开始一位一位地比较ascii码,若前面全相同,则比较长度,长度小的小于长度大的
14.六种常见容器的属性
- vector
- 是可变大小的数组容器(动态分配数组),比实际用到的空间占用更多空间(为后续扩充作准备),采用连续的存储空间
- 访问效率高,对不在末尾的删除和插入操作效率低
- list
- 是线性双向链表,每个节点包括一个信息块、一个前驱指针、一个后驱指针,采用非连续的存储空间,相比vector占更多内存
- 可以在中间快速删除、插入,不能根据下标访问,要按链表走下去
- deque
- 双向队列,采用分块的线性结果存储,所有deque块采用一个map块管理,底层可以看作是一个指针数组,指针分别指向一个不连续的内存空间
- 首尾都能高效删除、插入,支持随机访问,中间元素删除、插入效率低
- priority_queue
- 有序的堆
- set
- 底层用红黑树实现,存储包含元素的节点而不是存元素本身,不允许重复,每个元素包含一个关键字,支持关键字查询(即检查给定的关键字是否在set中)
- map
- 底层用红黑树实现,存储包含元素的节点而不是存元素本身,以键值对的形式存储,关键字起索引作用
- 红黑树就是一个平衡的二叉搜索树(右值大于节点值大于左值),严格说又不平衡因为左右子树高可能大于1:
- 每个节点或者是黑色,或者是红色
- 根节点是黑色
- 每个叶子节点(NULL)是黑色,这里的叶子节点是空节点而不是没有左右子树的节点
- 如果一个节点是红色的,则它的子节点必须是黑色的
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
15.vector底层原理
- 底层是一个动态数组,包括三个迭代器start和finish之间是已经被使用的空间范围(size),start和end_of_storage之间是vector所占得所有空间(capacity,包括没有用到的备用空间)
- capacity空间不够时,会自动申请另一片更大空间(1.5倍或2倍取决于编译器),然后将数据拷贝过去并释放原来占据的空间
- reserve()改变capacity,resize()改变capacity和size可以传参赋值
16.vector中的erase()与algorithm中的remove()区别
- erase()删除了元素,迭代器不能访问,erase()也可以自动返回下一个有效的迭代器,所以可以用
1
it = vec.erase(it)
- remove()只是把元素移到容器最后面,迭代器迭代到最后还是能访问的
17.vector迭代器失效情形
- 插入元素到vector中,会导致内存重新分配,指向原内存的迭代器失效
- 指向的元素被erase(),迭代器失效
18.STL线程不安全情况的解决
- 对同一容器进行多线程读写操作时,要锁定该容器
- 当容器的迭代器(begin、end等)还在生存期时,要锁定该容器在调用容器的成员函数时,要锁定该容器
- 在调用容器的成员函数时,要锁定该容器
- 在容器上调用算法执行时,要锁定该容器
19.STL的基本组成
- 分配器给容器分配存储空间
- 算法通过迭代器获取容器中的内容
- 仿函数协助算法完成各种操作
- 配接器用来套接适配仿函数
- 头文件<iterator>、<algorithm>、<vector>、<deque>、<string>
20.迭代器iterator模式又称游标cursor模式,用于提供一种方法顺序地访问一个聚类对象的各个元素,而又不受对象内部表示类型的影响(一般用于list、vector、stack和ostream_iterator等)
- 迭代器返回的是对象引用,所以输出cout时要用*迭代器才表示输出对象的值
- 迭代器和指针区别在于:迭代器是一个类的模板,它里面封装了指针,提供比指针更高级的行为,功能类似于智能指针
21.size_type是vector、string定义的类型,其实就是unsigned int类型,常用来记录vector、string的长度(不是自动获取的,还是得自己写语句赋值,其实就等于unsigned int的别名)
- 意义是方便多平台泛用,它代表着该平台最长的的数据类型长度,目的是尽可能地保存够大的无符号数值
1
std::string::sizetype lens;
22.用vector
23.顺序容器vector、deque、list(双向链表)、forward_list(单向链表)、array(固定长度数组)、string
24.迭代器相关
- 迭代器.end()实际指向的是容器最后一个元素的后一个位置。即一般使用(xx.begin(), xx.end())的都是默认左闭合区间[begin, end)
- 如果容器为空的话,begin()返回的和end()一样都是尾后迭代器
- 尾后迭代器指示的是不存在的“尾后元素”,这类迭代器没有实际意义,仅仅是个标记
- 如需声明迭代器的话最好用auto,因为我们不清楚或不在意迭代器的准确类型
- 反向迭代器例如.rbegin()、.rend(),对反向迭代器++,会得到上一个元素
- const迭代器例如.cbegin()、.cend(),该迭代器只可读不可改变,当某容器是const时,对其调用.begin()也会自动得到const迭代器
- 用整个容器来拷贝需要要求两个容器都是相同类型,而用迭代器表示拷贝范围则可以是两个不同容器类型,因此迭代器也符合泛型编程的思想
- 迭代器解引用
1
2
3
4
5
6a = *it; //a获得it迭代器指向的元素的值
*it = 2; //将it迭代器指向的元素的值改为2
a = it->xx1 //a获得it迭代器指向的元素的成员xx1的值
a = (*it).xx1; //与上一条等价
25.array的使用
- 声明必须同时包含数据类型和大小,因此如果没有初始值的话就必须要求其数据类型要有默认构造函数
1
2array<int, 10> temple;
# 声明了temple是一个长度为10的int类型数组
26.容器的赋值函数.assign(),不适用于关联容器和array
- xx.assign(b, e),将xx中的元素替换为b和e所表示范围中的元素,b、e是迭代器,注意b、e不能指向xx中的元素
- xx.assign(n, t),将xx中的元素全替换成n个值为t的元素
27.swap()除了可以交换两个元素,也可以交换两个相同类型的容器(此时元素位置不会改变,改变的是容器的内部数据结构,即原先的迭代器等仍然有效)
28.容器比较大小是相同数据类型才能比较,比较逻辑和string一样,先比元素大小,再比长度
29.保证下标合法用at成员函数,如果下标越界的话at会抛出一个out_of_range异常
1 | cout << xx.at(2); |
30.改变容器大小的.resize()
1 | list<int> list1(10, 42); |
31.string类型的拷贝
1 | # cp是char数组或char*表示的字符串 |
32.string类型的搜索
- s.find(‘x’); //返回第一次出现x的下标
- s.rfind(‘x’); //返回最后一次出现x的下标
- s.find(‘x’, index); //返回从s[index]开始第一次出现x的下标
33.三个容器适配器,用于让某种事物的行为看起来像另一种事物
- stack,用底层容器deque只保留部分功能再封装实现
- queue,用底层容器deque只保留部分功能再封装实现
- priority_queue
34.find()操作的是迭代器,所以int数组完全也可以用find()来找值
1 | int nums[] = {27, 12, 10} |
35.一些常用的泛型算法(算法只会运行在迭代器上,不能直接改变容器size!!)
- acculate()求和
1
2
3int sum = accumulate(vec.begin(), vec.end(), 0); //第三个参数是和的初始值,也间接隐含了返回值类型和使用哪个加法运算符
string sum = accmulate(vec_str.begin(), vec_str.end(), string("")); - equal()判断两个容器内保存的元素是否都一样(其实只是判断了容器2>=容器1,需要自己加if语句来对容器2的size进行额外规定)
1
equal(vec1.begin(), vec1.end(), vec2.begin());
- fill()将范围内元素全填充成指定值
1
2
3fill(vec.begin(), vec.begin()+vec.size()/2, 0); //将vec前半元素重置为0
fill_n(vec.begin(), vec.size(), 0); //从vec.begin()开始对vec.size()个元素赋值0,注意仅仅是vector的reserve分配空间也不够,因为fill要求的是要实际有这么多个元素,否则还是用back_inserter()的形式 - copy()拷贝
1
copy(begin(a1), end(a1), begin(a2); //前提是a2本身要足够大,否则应用back_inserter()的形式,因为算法不能直接改变容器大小
- replace()替换
1
2
3replace(list1.begin(), list1.end(), x1, target); //将list1中所有值为x1的元素替换成值为target
replace_copy(list1.begin(), list1.end(), back_inserter(list2), x1, target); //list1不变,将替换后的结果保存到list2的后面 - unique()消除重复元素,因为unique()只是把不重复的放到了前面,然后返回重复序列的首位置,所以还需erase处理(注意不重复序列外的部分的值位置不确定,只能删除不能利用)
1
2auto temple = unique(vec.begin(), vec.end());
vec.erase(temple, vec.end()); //erase的删除范围是[temple, vec.end())
36.迭代器back_inserter指向的是容器末元素的后一个位置
1 | vector<int> vec; //空向量 |
37.lambda表达式,也叫匿名函数(没有函数名),看作是函数里面的函数
- 局部变量列表->返回类型{函数体}
- 局部变量列表通常为空,不为空时表示函数体内要用到这个之前代码定义的局部变量的值
- 参数列表要像函数定义一样写明数据类型和参数名
- 最简形式[]{return xx},即参数列表和返回类型可以省略,会自动根据函数体内的return内容决定返回类型
- 实例
1
2
3void box(vector<string> &words, vector<string>::size_type sz){
auto wc = find_if(words.begin(), words.end(), [sz](const string &a){ return a.size()
>= sz;}); //此时的a是words里面的元素,wc结果是words里面第一个长度大于等于sz的元素的迭代器 - 还可以在函数里这样使用
1
2
3
4
5void box(int target){
auto sum = [target](int a, int b){return a+b+target}; //此时的target是按值传递的,即会有一个副本,在之后改变target的值并不会影响该副本
cout << sum(1,1) <<endl;
return;
} - 默认捕获方式,使用默认捕获后会自动捕获函数体内需要使用的变量而不用手动声明
1
2
3auto sum = [&, c](int a){os << a << c;}; //此时默认捕获是引用&,由于os没声明所以采用默认的引用捕获,由于c单独声明了所以c是不同于默认捕获的值捕获
auto sum = [=, &os](int a){os << a << c;};//此时默认捕获是值捕获,由于c没声明所以采用默认的值捕获,由于os单独声明了所以os是引用捕获 - 如果想改变值捕获传的参数值,要在()和{}之间加mutable关键字
38.迭代器分类
- 头文件#include <iterator>
- 移动迭代器:用于移动的迭代器,即普通的迭代器,类似有begin()、end()等
- 反向迭代器:就是rbegin()、rend()这些带r的迭代器
- 插入迭代器:参数是某容器,会根据绑定的容器类型,自动调取容器自己的插入方法来添加元素
- back_inserter:调用push_back
- fron_inserter:调用push_front
- inserter:调用insert,支持第二个参数,第二个参数为要插入的位置的后一个元素
1
2
3list<int> list1 = {1,2,3};
list<int> list2;
copy(list1.cbegin(), list1.cend(), back_inserter(list2));
- 流迭代器:IO类型对象的迭代器,使用流迭代器可以用泛型算法从流对象读写数据
- istream_iterator
1
2
3
4istream_iterator<int> in_iter(cin), eof;
vector<int> vec(in_iter, eof);
cout << accumulate(in_iter, eof, 0) << endl; - ostream_iterator
1
2
3
4ostream_iterator<int> out_iter(cout, " "); // " "表示每次cout之间有一个空格
for(auto e:vec) out_iter = e; //打印vec中所有元素
copy(vec.begin(), vec.end(), out_iter); //使用copy()来打印vec中所有元素
- istream_iterator
39.关联容器,map和set的关键字都是只可读而不可更改的
- map相关
- map:关键字-值对,按key升序排列,有序
- multimap:关键字可重复的map,有序
- unordered_map:用哈希函数组织的map,无序,查找更快
- unordered_multimap:用哈希函数组织的multimap,无序
- map用法
1
2map<string, int> memo = {{"hhh", 2}, {"jjj", 6}};
memo[str] +=1;
- set相关
- set:关键字
- multiset:关键字可重复的set
- unordered_set:用哈希函数组织的set,无序
- unordered_multiset:用哈希函数组织的mutilset,无序
- set用法
1
2
3
4
5
6
7
8
9set<string> memo = {"The"、"the"、"world"};
if(memo.find(str) != memo.end()) cout << "str在set里面" << endl;
avaliable.insert(i); //把i添加进set里面
auto p = available.lower_bound(i % k); //表示用二分查找法原理去找到set里面大于等于i%k的第一个值
//注意这里的p是迭代器类型,如果找不到的话p就等于available.end(),找到的话值应该为p*
available.erase(p); //删除迭代器指向的元素
- multimap和multiset会把关键字相同的元素存在相邻区域,所有找某关键字时,先.find(“xx”)和.count(“”),然后以find为起始位置,count数为循环次数对迭代器进行++遍历即可
- map和set的共同方法
1
2
3
4
5
6auto t1 = memo.begin(); //memo可以是map也可以是set
while(t1 != memo.end()){
cout << t1.first << t1.second <<endl ; //set是cout << t1 << endl;
t1++;
}
40.pair类型
- 头文件<utility>
- 一个pair保存两个数据成员
- 定义和使用
1
2
3
4
5
6pair<string, vector<int>> p1;
pair<string, vector<int>> p2;
.....
string temple = p1.first;
vector<int> vec(p1.second.begin(), p1.second.end());
if(p1 == p2).... //只有first成员和second成员分别相等时才有p1==p2 - 返回值为pair的函数
1
2
3
4
5pair<string, int> box(){
......
if() return {v.back(), v.size()}; // 等同于make_pair(v.back, v.size());
else return pair<string, int> ();
}
41.字符串中插入的用法
1 | string str = "abcdefg"; |
42.逆序遍历map
- it->first来访问key,it->second来访问value
1
2
3
4map<int, int> memo;
for(auto it = memo.rbegin(); it != memo.rend(); it++){
if(it->second == it->first) return it->first;
}
42.map是按键从小到大排序的,unoredered_map是无序的,所有用auto& t:map遍历map时可以得到按键排序的结果
43.求nums全排列的库函数next_permutation返回值是一个bool值,当nums.begin()到nums.end()之间的元素已经没有不重复的排列方式时,返回false
- 注意next_permutation()在使用前需要对欲排列数组按升序排序,否则只能找出该序列之后的全排列数
- 即next_permutation对每个排列有一个自己的顺序,升序排列的序列处在这个顺序的开头
1
2
3
4sort(nums.begin(), nums.end());
do{
ans.push_back(nums);
}while(next_permutation(nums.begin(),nums.end()));
八、面向对象
1.class默认private
2.类的构造函数默认参数写法,表示只用输入一个参数即可构建一个Node实例,默认pre和next都是nullptr,其中Node*表示是指向Node对象的指针变量
1 | class Node{ |
3.析构函数:函数名= ~类名,不能带任何参数。没有返回值,不能重载
- 编译器总会自动生成一个缺省的析构函数,如果自定义了析构函数(当类中有指针时需要定义析构函数来释放内存空间),会先调用自定义的
- 析构调用顺序:子类析构函数->对象成员析构函数->父类析构函数
4.为什么某些类的析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?构造函数能不能时虚函数?
- 将可能被继承的父类的析构函数设置为虚函数,可以保证我们new一个子类并且使用基类指针指向该子类对象时,释放基类指针就可以自动调用子类的析构函数来释放子类空间,防止内存泄漏
- 默认析构函数不是虚函数,是因为虚函数需要额外的虚函数表和虚表指针会占用额外内存,因此非必须不需要将析构函数设为虚函数,不然会增加开销
- 构造函数不能是虚函数
- 虚函数表(在类中)和指针(在对象中)要占内存,而类没有实例化之前时没有内存空间可以分配的
- 虚函数用于信息不全的情况下,而构造函数本身就是要明确对象类型、要初始化实例的,使用虚函数没有意义
5.重载、重写和隐藏的区别
- 重载:函数名相同,参数不同,在同一作用域
- 重写(覆盖):子类中重新定义了父类的虚函数,虚函数表中地址会被替换为子类重新定义的方法,虚函数如果没有被重写的话,它就会被子类继承,表现得和普通函数一模一样
- 隐藏:子类中有与父类同名函数,只要同名不管其参数是否相同,父类的函数都会被隐藏
6.一个空白的类占1字节,里面有
- 合成的构造函数
- 合成的拷贝构造函数
- 合成的析构函数
- 合成的类拷贝赋值运算符=
- 缺省取址运算符
- 被const修饰的缺省取址运算符
1
2
3
4
5
6
7
8
9类名* operator&()
{
return this;
}
const 类名* operator&()const
{
return this;
}
(实际用上时,编译器才会去定义它们)
7.定义在类声明中的成员函数,会自动成为内联函数
8.类定义的语法
- 类的定义最后一个花括号后面必须有;
- 声明的函数最后一个花括号后面必须有;
- 实现的函数最后一个花括号后面不用加;
9.类的成员函数声明必须在类内,但是定义可以在类内也可以在类外
10.若用某个指针指向一个类,则其访问成员的方式要用->
1 | string s1 = "abc"; |
11.用static声明的静态成员只与类关联,而与以此类生成的实例化对象无关,即只有一份。可用类名::静态成员名这样的形式来访问,也可以用对象照常访问
- 通常情况静态数据成员不应在类内部初始化(非要在内部初始化的话要求初始值是常量表达式),因为避免每次实例化对象时构造函数都对其重复初始化,而应该在外部用以下形式初始化
1
double Account::interestRate = initRate(); //因为使用了域运算符::所以即使initRate()是类的函数也可以照常调用
- 访问形式
1
2
3
4
5
6
7
8# 已知Account类里有一个静态成员函数rate()
Account a1, *a2;
n = Account::rate();
n = a1.rate();
n = a2->rate();
# 以上三种方式等效 - 静态成员可以是不完全类型(指正在定义,但此时还没定义完成的类类型),顺便一提指针也可以是不完全类型,但是其他的数据成员都只能时完全类型
- 静态成员还可以用做该类的默认实参,非静态成员不行
12.this是一个常量指针,它指向当前的对象
- 由于this是隐式传递的,所以为表示成const int这样的形式来表示不能改变this指向的对象的值时,将const加在参数列表()后面
1
std::string isbn() const {return bookNo};
13.如果某成员函数最后返回的是 return *this,则说明返回的仍是调用该成员函数的对象
14.定义类相关的非成员函数:这部分代码要写在类外部,但是要和类在同一个.h文件内
- 参数应该包含Sales_data &item这样的形式,来引入对象
15.构造函数(可以写成有多种参数形式的同名函数)
- 只有没有自己写构造函数的情况下,才会调用默认构造函数(数组和指针不能用默认构造函数初始化),默认构造函数会对所有数据成员初始化(自己有定义初值=xx的用该值,自己没定义初值的用数据类型默认值)
- 要使用默认构造函数时用Sales_data temple,而不是Sales_data temple()
- Sales_data() = default表示不传任何参数,采用默认构造函数(在类内部这样使用时,默认构造函数是内联函数),当你自己写了一个构造函数后还想利用默认构造函数,那这条语句就是必须的
- Sales_data(const std::string s, unsigned n, double p): bookNo(s), unites_sold(n), revenue(p*n) {}表示用传入的s、n来分别给数据成员bookNo、unites_sold赋值,并通过p*n的计算来给revenue赋值
- 在类的外部定义构造函数用
1
Sales_data::Sales_data(){}
16.复构、赋值、析构
- 默认复构、析构函数在分配类之外的资源时往往会失效,例如动态内存的类
- 但很多需要动态内存的类应该使用vector或string来管理,这样能避免分配释放内存操作带来的复杂性,即有vector和string的类可以使用默认复构、赋值、析构,在该对象销毁时,vector和string会自动跟着销毁
17.用访问说明符public和private访问控制(即实现封装)
- public之后的成员在整个程序内都可以被访问
- private之后的成员只能被该类内的成员函数访问,即private用于封装了类的具体实现
- 如果想用类外的函数访问私有成员,需要在类内开头加friend再声明一遍该函数是它的友元函数,注意这个声明可以写在类内任意位置,因为它不受访问说明符控制
18.用来定义类型成员的语句,必须先定义才能使用(这点和函数成员与数据成员不同)
19.定义在类内部的成员函数是默认内联的,定义在类外部的函数需要用incline定义才能内联(建议用这种),如果在类内声明的时候已经用incline了也可以
20.开头加mutable声明的数据成员(可变数据成员),即使是const成员函数也可以改变它
21.若成员函数move()的返回值是类的对象的引用的的话
1 | temple.move(4,0).set('#'); |
22.函数的常量版本和非常量版本也可以通过基于const的重载来实现
- 已知类中有这两种display版本
1
2
3
4
5
6
7
8Screen &display(std::ostream &os){
os << contents;
return *this;
}
const Screen &display(std::ostream &os) const{
os << contents;
return *this;
} - 调用是会自动根据情况调用常量版本和非常量版本
1
2myScreen.display(cout).move(4,0).set('#'); //调用的是非常量版本,因为后面还要改变对象(如果没有非常量版本会报错)
myScreen.display(cout); //调用的是常量版本(注意如果没有常量版本的话,调用非常量版本也是可行的) - 常量版本也只是在执行同一条语句是不能连续使用.运算符而已,当写另一行时就可以改变对象的值了
23.实例化对象时
1 |
|
24.一个类只有完成定义后才能被使用,因为这样编译器才知道存储这个类的数据成员需要多少空间,但一旦一个类的名字出现就说明它被声明了,所以
- 类里面不能有该类的类型成员(因为此时类还没定义完全,编译器不知道这种成员需要多少存储空间)
- 类里面可以有指向该类的指针和返回值是该类对象的引用
25.在A类里指定B类是它的友元后,B类的成员函数就可以用A类对象.xx的方式访问A类的公有和私有成员了
1 | class A{ |
- 也可以用friend void B::clear(xx);的形式单独声明B类中的clear(xx)方法是A类的友元
- 注意友元关系不能继承(派生类的友元不能访问基类的成员)也不能传递
26.类的成员函数在类内声明,而在类外定义的原因
- 某些定义语句需要放到最后才能不报错
- 例如A类的方法a()里面需要用到一个类外函数f(),这样的话a()的定义就必须放到f()的声明之后,不然编译器执行时根本不知道f()是什么
27.在类外定义类的成员函数时,一旦出现了“类名::”,编译器就会自动将该函数定义的部分划分为处于类作用域下
- 即使本次定义中又用到类的另一个数据类型,也只需用“数据类型名”即可,而不用写成“类名::数据类型名”
- 可以访问私有成员(不光这个,只要是类的成员函数都可以访问私有成员)
- 有个例外是,如果返回值是类内定义的话,需要用“类名::返回值 类名::函数名(){}”这样要定义,因为在写返回值时编译器还没遇上“类名::”,还未将其划分在类作用域内,所以需要手动额外声明返回值是哪个类定义的
28.类内声明数据类型的格式
1 | using xx = std::vector<Screen>::size_type |
29.聚合类:用户可以直接访问其成员
- 所有成员都是public的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有虚函数
1
2
3
4
5
6
7
8# 定义
struct Data_sale{
int val;
string s;
};
# 使用
Data_sale temple = {0, "Anna"}
30.构造函数第一行用xx(xx1)直接初始化和在构造函数体内用xx=xx1来赋值(底层其实是先用数据类型的默认构造函数初始化,然后再赋值)理论上结果是一模一样的,但是如果成员变量是const或引用类型或某种没有默认构造函数的类类型,那么就只能初始化,而不能用赋值的形式了
- 初始化的值尽量用构造函数的参数,而非使用类的成员,因为要使用成员进行初始化时,该成员必须已经先一步初始化完成才能这样做,即需要多考虑初始化的顺序,反之全用构造函数参数的值做初始化就不用考虑顺序了
- 如果构造函数的参数列表里面已经有int xx=xx1这样的值了,即如果没有传参数xx的话也可以运行该构造函数,因为会默认传值为xx1的xx
- 即如果一个构造函数所有参数都提供了默认实参,则它实际上也定义了默认构造函数
31.一个类的大小计算
- 不够定义字节长的,要补足
- 出现virtual关键字的说明有一个虚函数表,要算4个字节
- static定义的成员占用字节数不算在类里
32.当一个父类指针指向一个子类对象并调用A()时,若父类方法中A()不是虚函数,则默认调用父类里的A()
33.拷贝构造函数
- 第一个参数是自己所处类的引用(参数一般请加const修饰)(所以不能是explicit的),且任何额外参数都是有默认值的构造函数
- 直接初始化和拷贝初始化区别
- 直接初始化,实际上是要求编译器使选择与我们提供的参数类型最匹配的构造函数
- 拷贝初始化,要求编译器将右侧运算对象的拷贝一份并给到正在创建的对象(通常由拷贝构造函数完成,如果有移动构造函数就由移动构造函数完成),实际上除了用=定义时会发生,在值传递时也会发生拷贝初始化
1
2
3
4string s1(10, '.'); //直接初始化
string s2(s1); //直接初始化
string s3 = s1; //拷贝初始化
string s4 = ".........."; //拷贝初始化
34.explicit关键字的作用就是防止类构造函数的隐式自动转换
1 | class T{ |
35.类赋值运算符=
- 定义,以左侧对象为操作主体this,右侧对象为参数xx
1
2
3
4类名& operator=(const 类名& xx){
....
return *this;
} - 如果没定义类赋值运算符=的话,会自动合成一个全盘复制的拷贝赋值运算符=函数
36.析构函数
- 定义,不接受参数,所以不能重载,所以一个类只能有唯一一个析构函数
1
2
3~类名(){
...
} - 析构函数首先执行它的函数体,然后再根据成员在类中的声明顺序逆序销毁(即先声明后销毁)
- 销毁不同成员的操作
- 动态分配内存,要函数体内delete销毁(因为属于了类外资源)
- 类类型的不用写代码(智能指针也属于类类型),会去调用它自己的析构函数
- 内置数据类型(包括普通指针)也不用写代码,直接销毁
- 三/五法则
- 三法则:需要自己定义析构函数的一定也需要自己定义拷贝构造函数和赋值操作符=。因为只有含有动态分配内存的类才需要自己定义析构函数,而如果使用默认的拷贝和赋值操作符=,会出现多个对象可能指向相同的内存(因为它们会默认将对象1的值
简单地复制一份就给对象2),而此时任意销毁一个对象,都会导致这个相同内存被释放,而导致其他对象访问出错 - 五法则:需要自己定义拷贝构造函数的类也需要自己定义拷贝赋值操作符=,反过来也成立
题外话:在较新的C++11标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”;也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的;为了统一称呼,后来人们干把它叫做“C++ 三/五法则”;
- 追加:如果任一定义了一个拷贝操作(拷贝函数、拷贝赋值符),那么就该定义拷贝构造函数、赋值操作符、移动构造函数、移动赋值运算符和析构函数这五个操作,因为定义拷贝操作意味着有需要额外操作的资源
- 三法则:需要自己定义析构函数的一定也需要自己定义拷贝构造函数和赋值操作符=。因为只有含有动态分配内存的类才需要自己定义析构函数,而如果使用默认的拷贝和赋值操作符=,会出现多个对象可能指向相同的内存(因为它们会默认将对象1的值
37.显式使用合成的函数和阻止类的某些功能
- 函数名() = default; //该函数使用合成函数
- 函数名() = delete; //称为删除的函数,阻止该函数,即若函数名是~类名则表示这个类不允许拷贝,例子iostream
- 主要用途是告诉编译器不要为此类合成该函数
- 在新标准发布之前,类是通过将拷贝构造函数和拷贝赋值运算符的函数放到private里面并且只声明不定义来实现阻止拷贝的
38.通过拷贝构造函数的不同写法,可以将类分为
- 行为像值的类:副本和原对象是完全独立的,改变副本不会对原对象有任何影响
- 行为像指针的类:副本和原对象使用相同的底层数据,改变副本或原对象对另一个有影响
39.swap()
- 如果类里面没有自定义swap()的话就会默认调用标准库的swap,标准库的swap是通过拷贝操作进行值交换,而不是交换地址
- std::swap()是指显式调用标准库的swap()
- 底层原理:先拷贝右侧对象,然后调用swap来交换副本和左侧对象
40.自己写一个实现vector
- allocator成员是非常重要的,通过调用其construct方法来在已划分的内存中构造新string元素
1
2static std:allocator<std::string> alloc;
alloc.construct(first_free, str); //意为在first_free处构造一个值为str的元素
41.右值引用&&——是为了支持移动操作提出的
- 只能绑定到一个将要销毁的对象
- move()可以将左值转换成右值
1
int &&rr2 = std::move(rr1);
42.移动构造函数
- 定义,第一个参数是该类对象的右值引用
1
2
3
4
5
6类名(类名 &&xx) noexcept : 成员1(xx.成员1), 成员2(xx.成员2){
xx.成员1 = xx.成员2 = nullptr;
}
# noexcept表示不抛出任何异常
# =nullptr是因为使用移动构造函数后原对象xx会销毁,而如果销毁时原对象指针仍指向动态分配的地址会导致其析构函数释放该空间,进而导致移动后的对象不能正常使用,所以要置为nullptr - 需要写一个移动赋值运算符=,和之前重载拷贝赋值预算符=一样,只不过参数换成了该类对象的右值引用类型
- 注意用if语句对自赋值进行额外判断
- 注意这里函数体也要加一条
1
xx.成员1 = xx.成员2 = nullptr;
- 已经定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数的类不会自动合成移动构造函数和移动赋值运算符,只有没有定义任何拷贝构造函数、拷贝赋值运算符并且每个数据成员(忽略static)都可以移动时,编译器才会自动合成移动构造函数和移动赋值运算符
- 移动构造函数永远不会隐式定义为删除的函数,除非该类不能移动,而你还非要定义一个=default的移动构造函数
- 如果既有移动构造函数,又有拷贝构造函数,则通过参数类型匹配来选择是移动还是拷贝(实现了push_back的容器都会有两种版本的push_back,自动根据参数选择拷贝还是移动)
1
2
3T t1, t2;
t1 = t2; //拷贝,t2是一个对象,是左值
t2 = xx(xx1); //移动, xx()返回的是一个值,是右值 - 移动迭代器作用:移动后让原迭代器的所有其他操作在移动迭代器中都照常工作,即移动后对“make_move_iterator(原迭代器)”操作,就相当与移动前对“原迭代器”操作
1
make_move_iterator(原迭代器); //该语句会返回一个移动迭代器
43.在函数参数列表()后加引用限定符&表示this只可以指向一个左值,加&&表示this只可以指向一个右值,这个是为了阻止s1+s2=“abc”这样向右值赋值的情况(只能用于非static函数,并且要同时出现在声明和定义中)
- 注意,&和&&也可以写两个同名函数然后实现重载
- 如果一个同名函数用&或&&显式声明了,那么它的所有同名函数也要同样用&或&&显式声明
44.已知父类father和public继承father的子类child,父类中有一个公有成员函数box()
- 子类对象使用.box()时会优先使用子类中定义的box,无论此时父类的box()是否由virtual声明;若子类中没有定义的box,则会使用父类中的box(),即使此时box()用virtual声明但它有定义的话就能正常调用
- 父类指针指向子类对象后调用box() (注意子类指针不能指向父类对象,不然会编译不通过)
- 若父类中box()是用virtual声明,则使用子类中的box()
- 若父类中box()没用virtual声明,则使用父类中的box()
- 若子类中没有box(),则无论父类box()是否用virtual声明都会调用父类中的box()
- 因为child是public继承father的,所以完全可以把child对象看成是father对象,即类似box(father f1)这样的函数也可以直接传child对象进去(ps:这个概念也称作动态绑定或运行时绑定)
- 只有public继承的才可以,private和protected都不行
- 提醒不加继承关键字的默认是private继承
45.无论是public、private还是protected继承,子类的方法都不能访问父类的私有成员,即继承关键字只影响父类公有成员和保护成员在基类中的存在形式
- 保护成员和私有成员一样只能被该类的成员函数和友元函数访问,区别在于保护成员对于子类而言是可见的
- 注意如果只是对子类的声明(即没有写定义),不要加:public 父类名这一部分语句
- 如果我们要把某个类用作基类,那么这个类必须已经定义而非仅仅是声明
46.子类重写父类中的函数时一般加override,虽然说不加也不怎么影响使用,但是加的话在编译时就会去检查该方法是否覆盖了父类的虚函数,如果没有就会报错,相当于为我们发现一些错误,加了一层保险
- 即标准形式为,父类方法用virtual声明,子类重写的方法的参数列表后要加一个override关键字
- 标准形式是为了保证这样的情形:
- 父类中方法参数和子类中同名方法的参数不同
- 按照设计逻辑应该是子类的方法重写父类的方法,但是如果不使用virtual声明的话子类就会把父类函数直接隐藏起来,这一结果明显不符合设计的出发点,所以开发中要求使用标准形式
- 注意一个函数一旦被声明virtual,那么子类中重写它的函数也全部是虚函数,子类虚函数的返回类型和参数必须和父类一模一样(除非返回的是类本身的指针或引用)
- 如果参数不一样的话,就不能通过父类的引用或指针调用子类的虚函数了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Base{
public:
virtual void box();
};
class C1:public Base{
public:
void box(int); //其实这里根本就没有重写,只是把Base里的box隐藏了,其实Base和C1里的box()都是存在的
}
int main{
Base B1;
C1 c1;
Base *p1 = &c1;
c1.box(1); //会报错
}
- 如果参数不一样的话,就不能通过父类的引用或指针调用子类的虚函数了
47.继承相关
- 子类继承自父类的数据成员,在子类初始化要使用用父类的构造函数对这些成员初始化,即每个类控制它自己的成员初始化过程
- 如果父类定义了一个静态成员,则无论派生出多少子类,都只存在该成员的唯一的实例
- 如果一个类不想被继承,就加上final
1
2
3class 类名 final{
...
};
48.继承关系中有静态类型和动态类型的概念,例如用一个父类指针指向一个子类对象,那么这个指针的静态类型就是父类指针,这个在编译时就是已知的,它的动态类型则是子类对象,这个是要在运行时才可知
49.若我们明确知道某个父类指针或引用转子类是安全的可以用static_cast来强制覆盖编译检查,否则不论是显示转换还是隐式转换编译都会报错
1 | farher f1; |
- 注意这里说的是指针和引用才可以互相转换,具体的不同类之间的对象是不存在转换的,能运行的父类对象=子类对象,其实原理还是用子类对象中的父类成员的值去赋给父类对象,即父类对象等于子类对象会将父类对象的成员值更新为该子类对象含有的父类成员的值
1
2
3
4
5
6father f1; //此时f1的a默认为1
child c2;
c2.a = 2;
f1 = c2; //此时f1的a变成了c2的a的值,即2
//也可以写成
father f1(c2); //表示用c2的值来初始化f1 - 父类指针无法调用子类独有的数据成员
50.虚函数的一些特殊使用
- 虽然子类的虚函数默认参数可以和父类不一样,但在设计中应该尽量一样
- 强行调用不属于自己类类型的虚函数(只能往上不能往下,即子类可以强行调用父类的,父类不能强行调用子类的)用域作用符::
1
2
3child c1;
cout << c1.box(1,2) << endl; //结果为-1
cout << c1.father::box(1,2) << endl; //结果为3 - 声明时用一个=0表示该函数是纯虚函数
1
2
3...
virtual void box(...) =0;
...
51.抽象基类
- 含纯虚函数的类是抽象基类
- 不能创建一个抽象基类对象,因为里面有纯虚函数,所以不能定义对象
- 如果一个抽象基类的子类没有覆盖抽象基类其中的纯虚函数,那么这个子类对象也不能被定义
52.using关键字改变个别成员访问权
- using 声明的数据成员的访问权取决于该using语句所处的位置
1
2
3
4B:private{
public:
using A::a1; //不用管a1本来应该是什么权限,这里声明后a1就是public的
};
53.在子类中重新定义一个基类已有的数据成员,会导致基类的数据成员被隐藏起来,编译器查找时都会优先使用子类中新的数据成员,除非使用域作用符手动指定了查找规则
1 | return A::a1; |
54.子类中如果有与父类同名函数,那么不会发生函数重载,而是父类函数直接被隐藏起来
55.如果父类中有box函数的多个重载版本,而子类只想对其中某一个参数不同版本进行改变的话,用using声明
- 即先用using把父类中的box所由重载版本包含进子类来(不写参数括号)
- 再重新定义某一版本
1
2
3public:
using box;
void box(int, string){...}
56.子类中拷贝和移动构造函数需要加一个:父类()
1 | class Base{...}; |
57.构造函数的继承相关
- 父类构造的默认实参不会被继承,甚至子类会直接省略这个参数,即如果父类构造函数两个参数,其中一个有默认参数,子类继承时继承的是一个只有一个参数的构造函数
- 想要继承非直接父类的构造函数用以下形式,注意和普通成员不同,这里using语句出现的地方并不会影响构造函数的访问权限,即它在原来的类里面是什么成员在这里就还是什么成员
1
using 类名::类名;
58.在vector等容器存放具有继承关系的对象
- 通过在容器中使用指向对象的智能指针来实现,这个智能指针类型应该是基类类型
1
2
3
4//Bulk_quote是Quote的子类
vector<shared_ptr<Quote>> box;
box.push_back(make_shared<Bulk_quote>("111", 1, 2));
//这样做的好处是此时还能通过box中的指针调用只属于Bulk_quote的函数
59.一个类的大小只与以下有关(空白类大小为1),与成员函数无关
- 数据成员
- 指针4字节
- 虚函数4字节
60.多重继承的注意点
- 如果子类包含了两个相同父类的实例,会产生warning信息
- 如果子类继承自两个不同父类,就不能用父类指针来指向该子类对象了
61.重载运算符的返回类型要注意
- 重载=,返回类型是对象的引用
- 重载+=,返回类型是void
62.注意用new创建的对象返回的是指针
1 | father f1 = new father; // 错误! |
63.注意对已经释放的堆内存第二次delete报错和对指向栈对象的指针delete报错一样
64.类内成员变量的初始化顺序
- 未实例化之前静态变量成员就先初始化,此时父类的优先于子类的;实例化之后非静态成员才开始初始化
- 成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关
九、泛型编程
1.OOP编程和泛型编程的不同之处
- OOP编程在程序运行之前都不知道类型
- 泛型编程在编译时就能知道类型了
2.函数模板就是一个公式,用来生成针对特定类型的函数版本
- 定义,模板参数类型关键字用typename和class都行,没区别,用typename更直观但一些老版本可能会有class(此时只能发现语法错误)
1
2
3
4template <typename T> //typename修饰,表示T是一个类型名
int box(T a, T b){
....
} - 使用时编译器会自动根据传参的类型生成模板的各种实例,运行到实例化时才会去检查类型错误
1
2cout << box(1, 2) << endl;
cout << box(0.4, 0.6) << endl; - 将返回值类型也设为模板定义的类型需要在函数声明前先定义模板(用typename和class都行,这里没区别)
1
2
3
4template <typename T> T box(T* p){
T tmp = *p;
return tmp;
} - 使用非类型参数,一个非类型参数表示的是一个值而不是一个类型。当一个模板被实例化时,非类型参数被一个用户或者编译器推断出的值所代替
1
2
3
4
5
6
7
8//定义
template<unsigned N, unsigned M>
int box(const (&p1)[N], const char (&p2)[M]){
....
}
//使用
cout << compare("hi", "mom") << endl; //此时的N会被推断成3,M会被推断成4 - 一个非常典型的应用
1
2
3
4
5
6
7
8template<class class_type, class data_type1, class data_type2>
char* box(data_type1 class_type::*mem1, data_type1 class_type::*mem1){
return mem1 < mem2 ? "mem1 occure first" : "mem2 occure first";
}
box(&Point3d::z, &Point3d::y); //z和y是类Point3d中的float成员
//class_type会被绑定为Point3d,而data_type1和data_type2会被绑定为float
3.类模板
- 定义
1
2
3template <typename T> class Blob{
...
} - 类模板里面如果要使用到包含T的数据结构(包括定义函数、定义别名、定义参数和定义变量等等),也应该加上一个typename关键字
1
typedef typename std::vector<T>::size_type size_type;
- 使用时要额外用一个<>来提供类型信息
1
Blob<int> blob1; //blob1对象里面的每个T都会被编译器替换成int
- 定义在类模板以外的成员函数前面要加temple<模板参数>这样的关键字
1
2
3template <typename T> void Blob::func1{
...
} - 有一种比较傻的用法,在类模板声明时直接给值其实就是默认模板类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//定义
template<typename Real = double, int Dim1 = 128>
class KMeans_plus1{
...
}
//使用
Kmeans_plus1<> km;
//当然也可以这样用
Kmeans_plus1<int, 2> km;
//等同于
template<typename Real, int Dim1>
class KMeans_plus1{
...
}
Kmeans_plus1<double, 128> km; //类模板思想其实有点像函数传参,只不过更符合泛型编程 - 模板要在编译时就确定,所以模板参数不能是Kmeans_plus<double, x> km;这样用变量x去指定int值
十、内存分配
1.c++程序的内存
- 数据段:又叫静态内存,保存局部static对象、类static数据成员和定义在函数之外的变量(全局变量),使用之前就已经分配好(后声明先释放)
- BSS段放未初始化全局变量
- 可读可写数据段放已初始化全局变量
- 代码段:用来放代码,这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读
- 栈内存:保存函数内的非static对象,运行到这条代码时才分配内存(后声明先释放)
- 堆内存:又叫动态内存和内存池,由new和delete控制,只有它需要手动销毁
2.标准库通过两种智能指针来安全地使用动态内存
- 头文件<memory>
- shared_ptr:允许多个指针指向同一对象
- unique_ptr:一个对象只能由一个指针指向
- 还有一个weak_ptr的伴随类,用来指向shared_ptr所管理的对象
3.shared_ptr的使用
- 定义,和vector一样
1
2
3
4
5
6
7
8shared_ptr<string> p1; //可以指向string
shared_ptr<list<int>> p2; //可以指向list<int>
//最安全的动态内存分配方法:使用make_shared()
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10, '9'); //等效于make_shared<string>("9999999999")
//更常用下面这种形式
auto p5 = make_shared<vector<string>> (); - 检测指针的共享对象数目
- p.use_count()返回共享对象数目,很慢,一般不用
- p.unique()若共享对象数为1则返回true,否则返回false
- .reset()用法
1
2
3p.reset(); //若p是唯一指向对象的shared_ptr指针,则将其释放并将p置为空
p.reset(q); //同上,只不过释放后将p指向q
p.reset(q, d); //同上,只不过释放q时会调用d,而不是默认的delete - shared_ptr和局部变量一样,离开作用域时会自己销毁
- 每当一个shared_ptr被拷贝或当成值赋给其他变量时,内置计数器都会自动+1,当指向该对象的一个shared_ptr被销毁时,share_ptr类的析构函数会对计数器-1,为0时就去销毁这个对象(调用这个对象的析构函数)来释放动态内存
4.vector只是一个用于管理动态内存的类并不实际占多少内存,它里面含三个指针指向堆,每当push_back()时就用里面封装的allocator来申请新内存
5.局部定义的对象在局部执行完毕后,会全部自动销毁,除非还有一个全局变量使用到它的底层元素
1 | Blob<string> b1; |
6.new和delete
- 和在函数内定义内置类型一样,用new声明的内置类型也是未初始的(包括内置类型的数组),需要手动赋值或者用以下形式默认初始化
1
int *p1 = new int(); //此时*p1 = 0
- delete销毁的是指针指向的对象,可以释放==nullptr的指针,注意不要用delete释放指向局部变量的指针或已经释放过的指针(某些编译器会通过,但是是错误的)
1
2delete p1;
delete [] p2; //删除一个动态数组,销毁的时候是逆序销毁,即从尾部元素开始销毁
7.new出来和直接声明的区别(最大区别就是new要用指针接受,且有全局性)
- new出来的对象需要使用指针接收,而直接声明的不用。
1
2int* a=new A();
int a(); - new出来的对象是直接使用堆空间,而局部声明一个对象是放在栈中。
- new出来的对象类似于申请空间,因此需要delete销毁,而直接声明的对象则在使用完直接销毁。
- new出来的对象的生命周期是具有全局性,譬如在一个函数块里new一个对象,可以将该对象的指针返回回去,该对象依旧存在。而声明的对象的生命周期只存在于声明了该对象的函数块中,如果返回该声明的对象,将会返回一个已经被销毁的对象。
8.改善动态内存管理的错误
- 内存泄漏:一定要记得delete
- 使用已经释放过的对象(后果相当于使用未初始化的指针):delete后将指针置为空,方便判断
- 同一块内存释放两次:记得检查逻辑
9.shared_ptr和new的使用
1 | shared_ptr<int> p1(new int(42)); //正确 |
10.unique_ptr的使用
- 定义
1
2
3
4unique_ptr<string> p1(new string("aabb"); //正确
unique_ptr<string> p2(p1); //错误,不支持拷贝
p3 = p1; // 错误,不支持赋值 - .rest()和share_ptr一模一样,只不过不用判断是否为唯一指向对象的指针,而是直接释放
- 虽然不支持拷贝和赋值,但是有个例外:可以拷贝和赋值一个即将销毁的unique_ptr,即函数return unique_ptr指针,因为此时局部定义的unique_ptr必定即将销毁
11.weak_ptr的使用
- weak_ptr指向一个已经由shared_ptr指向的对象,但是不会增加计数器的值,且当计数器归0时即使任有weak_ptr指向该对象,也会无视而将对象空间释放,所以称为弱引用
- 定义
1
2auto p = make_shared<int> (42);
weak_ptr<int> wp(p); - 访问指向对象,weak不能直接访问,而是要调用.lock()使用其返回的shared指针来访问,若对象已经被释放,返回空的shared指针
12.new与malloc的区别(new面向对象,malloc面向内存)
- 属性:new/delete是C++关键字;malloc/free是库函数
- 参数:new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算;malloc则需要显式地指出所需内存的尺寸
- 返回类型:new返回的是对象类型的指针,故是符合类型安全性的而malloc内存分配成功则是返回void *
- 分配内存失败:new抛出bac_alloc异常;malloc返回NULL
- 自定义类型的分配内存:new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现);malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作
- 重载:C++允许重载new/delete操作符,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址;malloc不允许重载
- 内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间;malloc函数从堆上动态分配内存
- 自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中
13.Linux下报段错误
- 内存访问出错,访问到不属于你的内存,例如数组越界、指针越界、指针未初始化
- 非法内存访问,程序试图访问内核段内存
- 栈溢出,数组开得太大会超出栈的容量
14.检查内存泄漏最简单的方法就是检查new/malloc和delete/free是否匹配
- 复杂一些的程序使用额外工具辅助,但是其原理也是检查new/malloc和delete/free是否匹配,利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况
15.C++中new一个对象底层是这么做的
- 在堆区域内为对象划分内存
- 分配是在程序运行阶段完成的
16.浅拷贝和深拷贝
- 浅拷贝:只是将指向的地址拷贝给了新变量,实际它俩指向同一个内存空间
- 深拷贝:重新开辟一块内存空间拷贝原变量内存空间的值,并把这个新地址给新变量
- 基本类型不存在浅拷贝和深拷贝,因为基本类型直接传的就是值
十一、编译相关
1.GCC中有gcc(c编译器)和g++(c++编译器)
- 后缀为.c的文件,gcc当成c文件处理,g++当成c++处理
- 在编译阶段,g++会自动链接STL库,而gcc不会
- gcc在编译c文件时,可用的预定义宏是比较少的
2.C++函数栈空间最大默认1M
3.C++内存分四个区
- 动态区(const修饰的变量)
- 栈区:编译器自动分配释放,类似栈
- 堆区:程序员分配释放,类似链表,原理是利用记录空闲内存地址的链表
- 静态区(static修饰的变量)(只读数据段):存放全局变量、静态变量,程序结束后释放
- 文字常量区
- 程序代码区
4.const和#define区别
- const在编译运行时,#define在编译预处理时
- const有对应数据类型,需要进行一步判断,#define只是简单的字符串互换
- const只占一份空间,#define则是有多少地方使用就展开多少次,占多少份空间
5.sizeof和strlen的区别
- sizeof是操作符、关键字,编译时就已得到结果;strlen时库函数,要运行时才进行计算
- sizeof参数可以时数据的类型(int、double)、变量(a、b),strlen参数只能时字符串
- 数组传入sizeof不退化,传入strlen退化为指针
6.标准宏MIN的写法
1 | #define min(a,b)((a) <= (b) ? (a):(b)) |
7.若一个类文件.h中定义类时有一个库
8.类定义通常还要写如下形式
1 | #ifndef TEMPLE_PRACT_SALES_DATA_H |
#ifndef表示变量未定义为真时,执行后面语句直到碰到#endif;#define表示将该名字设定为要进行预处理的变量(它们都称为头文件保护符,建议所有类定义都要加,为避免名字冲突应全大写)
- 即第一次包含Sales_data.h文件时会去预处理定义,后续就不再需要了
9.编译器分两步处理类(所以成员函数完全可以使用后面才定义的成员)
- 第一步先处理成员的声明
- 第二步才处理成员函数体
10.#include中库函数用<>,自己写的.h文件用””
11.字节对齐(也叫内存对齐)
- 32位机器默认按4字节对齐,64位机器默认按8字节对齐
- 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍
- 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍
- 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),除非有某个成员的大小大于n,那么这句话就不起作用
- 三个准则
- 结构体变量的首地址能够被其对齐字节数大小所整除。
- 结构体每个成员相对结构体首地址的偏移都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足。
- 结构体的总大小为结构体对齐字节数大小的整数倍,如不满足,最后填充字节以满足。
- 为什么要字节对齐
- 因为尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的。它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度。
- 假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作
- 有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
12.0x开头的地址表示是16进制!!
13.工程中.h声明类的所有函数和数据成员(以及全局变量等),然后用同名.cpp文件里面写详细实现
- .h文件中#include要用的库
- .cpp文件只需要#include “xx.h”即可
- 在main中调用类也只需#include “xx.h”即可
14.如果一个变量的声明区域在.h的类和结构体定义外,那么它就是一个全局变量了,任何#include该.h文件的代码都能使用该变量,所以也有用一个global.h文件来专门写全局变量的常用方法
15.编译环境位数决定指针大小,即64位编译器中指针占8位字节
- 类的字节对齐是按里面的最大成员来的
- 如果全是char,那么就按1字节对齐
- 如果有一个int,那么就按4字节对齐
- 如果有一个指针(包括虚函数),那么就按8字节对齐
16.代码执行文件
- 预处理:.cpp文件和.h头文件生成xx.i、xx.ii文件,宏#define的替换字符串在此进行
- 编译期:生成汇编代码.s,const、没有多态性质的类指针在此处理,此次不会检查内存错误
- 汇编:生成xx.o和xx.obj文件,即将汇编代码转换成机器码
- 链接:将静态库文件和xx.o、xx.obj文件链接成可执行文件
- 执行期:操作系统运行可执行文件,多态性质的类指针在这里才有准确指向,内存问题也在此步报错
17.静态库和动态库
- 静态库被使用目标代码最终和可执行文件在一起(它只会有自己用到的);而动态库的目标代码在运行时或者加载时才链接
- 如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份;而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大
- 静态链接的可执行文件要比动态链接的可执行文件要大,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息
- 如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署
- 静态链接的要比动态链接加载更快
十二、C++11新特性
1.数据类型long long是在C++11中新定义的,规定一个long long至少和一个long一样大
2.委托构造函数:使用所属类的其他构造函数来执行自己的初始化过程,即把自己的任务委托给其他构造函数
1 | Sales_data (std::string s, int i, int j): bookNo(s), price(i), count(j){} |
3.对象移动、智能指针和vector也是新特性
十三、待解决问题
1.使用sort自定排序规则时去掉if(a[0] == b[0])这一行代码,会报错 reference binding to null pointer of type ‘int’
1 | sort(intervals.begin(), intervals.end(), [](auto& a, auto& b){ |
2.使用Sales_data temple();想实验默认构造函数赋值时,会提示
1 | error: request for member 'bookNo' in 'temple', which is of non-class type 'Sales_data()' |
附:C语言函数
1.str相关函数
- char *strcat(char *dest, const char *src),把src连接到dest后面去,返回dest
- int strcmp(const char *str1, const char *str2),str1>str2返回正数,等于返回0,小于返回负数
- char *strcpy(char *dest, const char *src),把src复制一份连接到dest后面去,返回dest
- unsigned in strlen(const char *str),返回str长度(不算/0)
- char *strchr(char *str, char c),返回str中首次出现c的位置,没有的话就返回NULL,strtchr()是找最后一次出现c的位置
- char *strrev(char *str),翻转字符串并返回翻转的字符串
- char *strstr(const char str1, const char *str2),返回str中首次出现c的位置,没有的话就返回NULL
- char *strtok(char *str, char c),分割字符串,返回从字符串开头到c的子串,若想以改变后的父字符串基础上上找到第二个分割子串,则在第二次调用时应用strtok(NULL, c)的形式,即strto会自动保存上一次调用的地址
1
2
3
4
5
6str = "html.http."
str1 = str(str, '.');
str2 = str(NULL, '.');
str3 = str(str, '.');
# str1="html", str2 = "http", str3="html";