C++ 万字长文第一篇---拿下字节面试
关键字的作用
全局静态变量:在全局变量前加上 ,该变量作用域从定义该变量开始到文件尾结束。存储在静态存储区中。 如果 修饰的全局变量在 文件中,则其他文件也可以访问。如果在其他 中操作该变量,该变量会被真实改变。而在其他 中操作该变量,该变量的变化只在当前文件有效,虽然他们有着相同的初始内容,但是存储的物理地址却不一样。 局部静态变量:在局部变量前加上 ,该变量作用域只在该语句块或函数内。存储在静态存储区中。 静态函数:函数默认都是 类型的,在工程内声明某个函数,若其他 文件中同名函数,则会引起冲突。在函数前加上 可以保证该函数只被该 文件可用。 类的静态成员(函数):在类成员(函数)前加上 ,该成员(函数)则是所有对象的共享成员(函数),可用直接通过 <类名>::<静态成员(函数)> 调用。
静态变量的初始化
int main() {
int initNum = 3;
for (int i=1; i<=5; i++) {
static int n1 = initNum;
n1++;
printf("%d\n", n1);
}
return 0;
}
int main() {
int initNum = 3;
for (int i=1; i<=5; i++) {
static int n1 = initNum;
{
int *p = &n1;
p++;
*p = 0;
}
n1++;
printf("%d\n", n1);
}
return 0;
}
通过上面两份代码会发现第一份代码的 只被初始化了一次,这是符合逻辑的,而第二份代码的 却被初始化了五次。
原因在于静态变量是通过静态变量后面的一个内存位作为记录标志该静态变量是否被初始化的,所以 其实就是每次把这个内存位置为空,让程序以为他还没有被初始化。
和 ++的区别
++ 是面向对象的语言, 是面向过程的语言。 ++ 具有 封装、继承、多态 三种特性。 ++ 具有类型安全功能,例如强制类型转换。 ++ 支持范式编程,例如模板类。
指针和数组区别
指针保存数据的地址,数据直接保存数据。 指针先得到地址,在通过地址访问数据,数组直接访问数据。 同类型指针可以直接赋值,数组只能一个个赋值。 指针常用于动态数据结构,数组则用于固定数目,类型相同的元素。 指针的内存分配通过 动态分配,数组的内存在编译时分配好。 位系统下,指针的 都是 ,数组的 取决与数组所占空间的内存。
野指针
指向已删除的对象或者没有访问权限的内存的指针。
主要成因有三:指针没有被初始化、指针被 或者 后没有置为 、指针操作超出了所指对象作用范围。
指针(*)和引用(&)的区别
指针有自己的空间,引用不占空间。 指针是一种变量,引用是另一个对象的别名 指针的 取决与操作系统,引用的 取决于引用对象所占空间。 指针可以初始成 ,引用的初始必须是另一个对象。 指针可以变换所指对象,而引用始终是一个对象的引用。 指针存在多级指针,而引用只有一级。
函数指针
在编译过程中,每一个函数都有一个入口地址,而函数指针就是指向该入口地址的指针。
using namespace std;
void fun1(int x) {
cout << x << endl;
}
void fun2(int x) {
cout << x+x <<endl;
}
int main() {
void (*pf)(int);
pf = fun1;
pf(222);
pf = fun2;
pf(222);
}
多态性和虚函数(virtual)
using namespace std;
class Shape {
public:
void show() { // 未定义为虚函数
cout << "Shape::show()" << endl;
}
void virtual show() { // 定义为虚函数
cout << "Shape::show()" << endl;
}
};
class Line : public Shape {
public:
void show() {
cout << "Line::show()" << endl;
}
};
class Point : public Shape {
public:
void show() {
cout << "Point::show()" << endl;
}
};
int main() {
Shape *pt;
pt = new Line();
pt->show();
pt = new Point();
pt->show();
return 0;
}
/*
未定义为虚函数时输出结果
Shape::show()
Shape::show()
定义为虚函数时输出结果
Line::show()
Point::show()
*/
纯虚函数
有些情况下,基类生成的对象是不合理的,比如动物可以派生出狮子、孔雀等,这些派生类显然存在着较大的差异。那么可以让基类定义一个函数,并不给出具体的操作内容,让派生类在继承的时候在给出具体的操作,这样的函数被称为纯虚函数。含有纯虚函数的类成为抽象类,抽象类不能声明对象,只能用于其他类的继承。
纯虚函数的定义方法为:
void ReturnType Function() = 0;
子类可以不重写虚函数,但一定要重写纯虚函数。
静态函数和虚函数
静态函数在编译时就确定了调用它的时机,而虚函数在运行时动态绑定,虚函数由于用到了虚函数表和虚函数虚函数指针,会增加内存使用。
构造函数和析构函数
构造函数在每次创建对象的时候调用,函数名称和类名相同,无返回类型,构造函数可以为类初始化某些成员。 析构函数在每次删除对象的时候调用,函数名称和类名相同,但在前面加了一个 符号,同样无返回类型。若对象在调用过程中用 动态分配了内存,可以在析构函数中写 语句统一释放内存。 如果用户没有写析构函数,编译系统会自动生成默认析构函数。 假设存在继承:孙类继承父类,父类继承爷类 孙类构造过程:爷类 -> 父类 -> 孙类 孙类析构过程:孙类 -> 父类 -> 爷类
析构函数和虚函数
可能作为继承父类的析构函数需要设置成虚函数,这样可以保证当一个基类指针指向其子类对象并释放基类指针的时候,可以及时释放掉子类的空间。 虚函数需要额外的虚函数表和虚函数指针,占用额外的内存,所以不会作为继承父类的析构函数不用设置成虚函数,否则会浪费内存。 ++默认的析构函数不是虚函数,只要当其作为父类的时候,才会设置为虚函数。
重载、重写(覆盖)、隐藏
重载是指在同一访问作用域中,声明几个参数列表不同的同名函数,根据参数列表决定调用哪个函数,和函数返回值无关。 重写是在派生类中重新定义的函数,其函数名、参数列表都需要和基类中被重写函数一致,并且基类中被重写函数必须是虚函数。 如果基类和子类有同名函数,那么如果函数 参数列表不同 或者 参数列表相同但是没有 关键字 时,子类对象都会隐藏基类的函数。
在 函数前后执行函数
可以通过 attribute 关键字,声明 constructor 和 destructor 来实现。
using namespace std;
__attribute((constructor)) void before_main() {
cout << __FUNCTION__ << endl;
}
__attribute((destructor)) void after_main() {
cout << __FUNCTION__ << endl;
}
int main() {
cout << __FUNCTION__ << endl;
return 0;
}
虚函数表
在有虚函数的类中,存在一个虚函数指针,该指针指向一张虚函数表,当子类继承基类的时候,也会继承其虚函数表。当子类重写基类中的虚函数时,会将虚函数表中的地址替换成重写的函数地址。
char* 和 char[] 的区别
char *s1 = "abc";
char s2[] = "abc"
以上两种定义方式直接输出结果,则都能正常输出。但修改 的内容会引起程序崩溃,而修改 的内容不会。
因为 是保存在常量区内,而第一种方式是利用指针直接指向常量区,第二种方式是通过数组将 复制出来,存储在栈区内,所以修改 的值不会崩溃而修改 的值会。
++ 程序内存结构
++ 程序的内存分区自低地址至高地址分别分为 代码区、常量区、静态(全局)存储区、自由存储区、堆区、栈区。
代码区存放用户代码。 常量区存放常量,这里的量不可被改变。 静态变量和全局变量存放在静态存储区内,一直到程序全部结束后才会释放内存。 自由存储区内存由 分配和回收。 堆区内存由 分配和回收,若申请了空间但忘记释放,容易造成内存泄漏。 栈区存放函数内的局部变量,函数参数。当数据过了作用范围后,系统就会回收内存, 一般是 , 一般是 。
++ 中常量
定义常量有两种方法,第一种是使用 定义,另一种是通过 修饰,常量不可被修改。全局对象存放在静态区内,局部对象存放在栈区内。
关键字作用
通过 修饰的变量,成为常变量,不可被修改。 通过 修饰的类成员函数,成为常函数,该函数不可修改类内的成员变量。 两个类成员同名函数,一个带有 ,一个不带 ,相当于重载,会根据类对象是否是 修饰的决定调用哪个函数。 通过 修饰指针
int x = 10;
const int *a = &x;
int* const b = &x;
其中 修饰的分别是 和 ,那么对于 而言,可以修改 的指向,而不能修改 所指向的值,对于 而言,可以修改 所指的值,而不能修改 的指向。
为什么函数参数入栈顺序从右到左
为了支持 不定长参数函数
int add(int num, ...) {
va_list valist;
int sum = 0;
int i;
va_start(valist, num);
for (i = 0; i < num; i++) {
sum += va_arg(valist, int);
}
va_end(valist);
return sum;
}
如果是从左到右入栈, 变量将在栈底,而不定长参数需要这个 来确定元素的个数,在栈底自然是取不出来的。所以通过从右向左入栈,可以获得不定长参数的长度。
和 中的枚举
中的枚举是不限定作用域的
enum color{red, green, blue, yellow};
enum color2{red, green}; // ERROR,因为 red 和 green 已经在 color 中定义过了
auto x = red; //OK,因为 red 没有限定作用域
auto y = color::red; //OK
中引入了强类型枚举,是限定作用域的
enum struct color{red, green, blue, yellow};
enum struct color2{red, green}; // OK,red 和 green 在不同作用域内
auto x = red; // ERROR,red 没有指定作用域
auto y = color::red;
强类型转换的优点在于
限定作用域的枚举类型将名字空间污染降低 限定作用域的枚举类型是强类型的,无法通过隐式转换到其他类型,而不限定的枚举类型可以自动转换为整形
宏定义和枚举的区别
枚举是一种实体,占内存。宏定义是一种表达式,不占内存。 枚举在编译阶段进行处理,宏定义在与编译阶段就完成了文本替换。
空类
如果一个空类不被使用,则在编译器什么也不做。
但空类还是带着一些默认的函数,这些函数只有被使用的时候才会产生,主要是六个函数
默认构造函数 默认析构函数 拷贝构造函数 赋值运算符 (operator=) 取址运算符 (operator&)(一对,一个非 的,一个 的)
隐式类型转换
表达式中,低精度类型向高精度类型发生转换。 条件语句中,非布尔类型向布尔类型发生转换。 初始化语句中,初始值向变量类型发生转换。 赋值语句中,右侧运算对象向左侧运算对象发生转换。 可以用 单个形参 来调用的构造函数定义了从 形参类型 到 该类类型 的一个隐式转换。注意 单个形参 并不是只有一个形参,可以有多个形参,但其他形参要有默认实参。
using namespace std;
class Node
{
public :
string s1;
int a;
Node(string s, int val=0) : s1(s), a(val) {
}
bool ok(Node other) const {
return s1 == other.s1;
}
};
int main()
{
Node x("xxxxx");
cout << x.ok(string("xxxxx")) << endl; // 隐式
cout << x.ok(Node("xxxxx")) << endl; // 显式
return 0;
}
extern "C"
在 和 ++ 混编的程序中,由于 不存在重载机制,无法区分两个同名函数,于是引出 extern "C" 语句块,告诉 ++ 编辑器这段代码按 标准编译,尽可能的保存 和 ++ 的兼容性。
函数调用过程
每一个函数调用都分配一个函数栈,先将返回地址入栈,在将当前函数的栈指针入栈,然后在栈内执行函数。
++ 函数返回值和返回引用的区别
函数返回值时,生成一个临时变量,返回该临时变量,然后在调用处把该临时变量赋值给左侧变量。 函数返回引用时,返回和接收应是 int& 类型,不能返回局部变量的引用。不生成临时变量,直接返回该引用。
using namespace std;
int x, y;
int get1()
{
cout << "get1 中 x 的地址" << &x << endl;
return x;
}
int& get2() {
cout << "get2 中 y 的地址" << &y << endl;
return y;
}
int main()
{
int x = get1();
cout << "main 中 x 的地址" << &x << endl;
int& y = get2();
cout << "main 中 y 的地址" << &y << endl;
return 0;
}
/*
get1 中 x 的地址0x4c600c
main 中 x 的地址0x6efef8
get2 中 y 的地址0x4c6010
main 中 y 的地址0x4c6010
*/
++中拷贝赋值函数的形参能否进行值传递?
不能,会造成无限循环。
using namespace std;
class Node
{
public:
int x;
Node(int a) : x(a) {
}
Node(Node& a){ // 方法1,正确
x = a.x;
}
Node(Node a) { // 方法2,错误,无法编译通过
x = a.x;
}
};
int main()
{
Node x(10);
Node y(x);
return 0;
}
如果使用方法1,在传入 的时候,构造函数处 是引用,可以正常赋值。
如果使用方法2,在传入 的时候,构造函数处 是形参, 需要再次调用的 本身的构造函数,往下也是同样的道理,将造成无限构造的处境。
动态内存分配
在执行程序过程中动态分配或回收存储空间的分配内存的方法。
一般使用 ,++ 一般使用 。
原型为 void *malloc(unsigned int size),开辟一块长度为 连续内存空间,返回 类型指针。失败返回 。 原型为 void free (void* ptr),释放动态分配的内存, 和 要配套使用。 返回分配内存单元的起始地址,需要存放在一个指针变量中。失败抛出异常 。 也是用来释放动态分配的内存,分为两种使用 和 。 和 要配套使用。
int *p = (int*)malloc(sizeof(int)*100);
free(p);
int *a = new int;
delete a;
int *q = new int[100];
delete[] q;
A* a = new A; a->i = 10; 在内存分配上发生了什么?
A* a, 是一个局部变量,类型为指针,所以操作系统会开辟 字节的空间分配给指针 。 new A,通过 在堆区申请类 大小的空间。 a = new A,将指针 的内存区域中填入在栈中申请到的类 的地址的地址。 a->i = 10,先找到 的地址,然后通过 存储的地址和 在类 中的偏移量得到 a->i 的地址,然后在该地址内完成赋值操作。
原理
通过两种方法分配内存
当分配内存小于 时,调用 完成,从堆顶往高地址分配对应的内存。 当分配内存大于 时,调用 完成,在堆和栈中间找到一块空闲的虚拟内存分配。
的分配都是在虚拟地址上的分配,具体分配的物理内存由操作系统决定。
和
是标准函数库 | 是 ++ 运算符 |
从堆分配内存 | 从自由存储区分配内存 |
需要显式指出分配内存大小 | 编译器自行计算 |
不会调用构造/析构函数 | 会调用构造/析构函数 |
返回无类型指针 () | 返回有类型指针 |
不可调用 | 可以基于 |
不可被重载 | 可以被重载 |
和
是对分配/回收单个对象指针的处理。 是对分配/回收对象数组指针的处理。 开辟的数组空间会多 字节来存放数组大小
++ 类的访问权限
公有的,可以被任意对象访问。 受保护的,只允许本类和子类的成员函数访问。 私有的,只允许本类的成员函数访问。
和 区别
成员、继承方式默认都是 的, 默认的方式是 的。 不可用于声明模板类,而 可以。
template
// 存在 template
// 不存在
和 区别
是共用体,任何时刻都只存放一个被选中的成员,而结构体存放所有成员变量。 对共用体不同的成员赋值,其他对象也被改变,原本的值就不存在了,而结构体不同成员并不影响。 内存分配按照结构内部最大成员,结构体遵循字节对齐原则。
++类内定义引用数据成员
不能使用默认的构造函数,必须提供构造函数。 必须在初始化列表中初始化,不能再构造函数内初始化。 构造函数的形参也必须是引用类型。
构造函数分为初始化和计算两个阶段,第一阶段对应初始化列表,第二阶段对应函数主体,引用必须在第一阶段完成。
using namespace std;
class Node
{
public:
int& a;
int b, c, d;
Node(int &x, int y, int z, int k) : a(x), b(y), c(z), d(k) {
}
};
int main()
{
int t = 1;
Node x(t, 2, 3, 4), y(t, 2, 3, 4);
x.a++;
cout << y.a << endl;
return 0;
}
引用 和 引用
引用是另一个 变量 的别名,必须初始化且一旦定义就不可改变。
引用和引用不同的点在于 引用是指向 常量 的引用。
const int x = 10;
const int &y = x;
const int &z = 20;
如此一来,由于 是 的,不可被改变,将 也声明成 的,防止了 的值通过 被改变。 引用也可以直接指向一个常量。
左值和右值
左值: 指向某内存空间的表达式,并且可以通过 & 获得其地址。
右值: 非左值即右值。
右值引用
实现了转移语义和精确传递。可以消除两个对象交互时不必要的对象拷贝,提高效率,可以更简明的定义泛型函数。
转移语义可以将资源从一个对象转移到另一个对象上,减少不必要的临时对象的创建、拷贝以及销毁。
精确传递就是将一组参数 原封不动 的传递给另一个函数,原封不动 不仅仅是 参数值 不变,还有 左值/右值 和 const/not-const。
using namespace std;
void func(int& x)
{
cout << "左值引用" << endl;
}
void func(int&& x)
{
cout << "右值引用" << endl;
}
void func(const int& x)
{
cout << "const 左值引用" << endl;
}
void func(const int&& x)
{
cout << "const 右值引用" << endl;
}
template<typename T> void fun(T&& x)
{
func(forward
(x)); }
int main()
{
fun(10);
int x = 0;
fun(x);
fun(move(x));
const int y = 0;
fun(y);
fun(move(y));
return 0;
}
/*
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
*/
左值引用和右值引用的区别
左值引用表达为 int&,右值引用表达为 int&&。 左值持久,右值短暂。除了 引用,左值引用绑定到左值,右值引用只能绑定到将要销毁的对象。
和
是映射, 是集合,都是通过红黑树来实现的。他们的操作行为,都是转调红黑树的操作行为。
中元素为 (键-值)key-value,关键字起到索引作用,值保存相关数据。 元素为 (键)key,值即是键, 允许修改 ,不允许修改 , 的迭代器是 的,不允许修改元素。原因: 和 是通过关键字排序来实现有序的,如果要修改,需要删除原本键值,在插入新的键值,每次操作后都需要重新调节平衡,这样破坏了原本的结构,使得迭代器失效,不知道指向改变前的还是改变后的位置。 支持下标操作, 不支持。 下标操作 ,是通过关键字 去查找,返回该关键字的值,若该关键字不存在,则会插入一个具有该关键字和 类型默认值到 中。
和
通过红黑树实现 | 通过 表实现 |
操作复杂度 级别 | 操作复杂度常数级别 |
内部有序 | 内部无序 |
适用于对顺序有要求的场景 | 适用于频繁查找的场景 |
基于 表,需要用 来解决冲突,所以查找和存储的时间大大减少,而代价是花费更多内存。
的组成
由六部分组成:算法、容器、迭代器、仿函数、适配器、内存分配器。
通过迭代器,可以实现对容器内容的读和写。 对于重载了 的类,可以实现类似函数调用过程,叫做仿函数。 适配器将一个类的接口适配成用户指定的形式,使原本不兼容的类可以一起工作。 内存分配器负责空间的配置和管理。
容器和容器适配器
容器适配器是对容器的一种再封装,容器适配器不支持迭代。
自带的容器有 ,同时还提供了一些特别的容器适配器,比如 。
区别在于 的底层实现用到了 , 的底层实现用到了 ,而 的底层实现没有用到别的容器。
迭代器和指针
迭代器就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部结构而达到遍历集合的效果。
迭代器不是指针,是类模板。迭代器只是模拟出指针的一些功能,本质是封装了原生指针,提供了比指针更高级的行为。迭代器返回的是对象的引用,而不是对象的值。
和
存在扩容机制:在向 添加元素时,如果还有剩余的空间,那么会直接添加到指定位置,如果没有剩余的空间,那么会重新开辟原本容器的两倍空间,然后将旧的数据复制到新开辟的空间,释放旧内存。
vector | list | |
---|---|---|
类型 | 动态数组 | 动态链表 |
底层实现 | 数组实现 | 双向链表实现 |
访问 | 支持随机访问, | 不支持随机访问, |
插入 | 在末尾 ,在中间 | 很快, |
删除 | 在末尾 ,在中间 | 很快, |
内存来源 | 从堆区分配空间 | 从堆区分配空间 |
内存使用 | 是顺序内存 | 不是顺序内存 |
内存分配 | 一次性分配好,不够时扩容 | 每次插入节点都需要进行内存申请 |
性能 | 访问性能好,插入删除性能差 | 插入删除性能好,访问性能差 |
适用场景 | 经常随机访问,不在乎插入和删除效率 | 经常插入删除,不在乎访问效率 |
中 和
改变当前容器内含有元素的数量。例如 $vector
改变当前容器可存放元素的最大容量,例如操作 ,如果 值大于当前容器容量 ,则会重新分配一块能存 个对象的空间,然后把容器内元素复制过来并销毁之前的内存,否则不会发生变化。
的底层原理
数据量大时使用快排来实现,然后分段递归。当数据量小于某个值后,为了避免递归带来的过大额外开销,使用插入排序。如果递归层次过深,就会改用堆排序。
释放内存
在插入大量数据后,即使把数据全部删除,但并没有改变容器的容量,仍然会占用内存。
通过 操作来释放 的内存,原理是 使用 的默认构造函数建立了临时对象,使用 操作让原本对象和临时对象交换内存, 完成后,临时对象就是容量非常大的 , 接着临时对象消失,释放内存。
using namespace std;
int main()
{
vector<int> g;
for(int i=1; i<=10; i++)
g.push_back(i);
cout << "capacity = " << g.capacity() << endl;
vector<int>().swap(g);
cout << "capacity = " << g.capacity() << endl;
return 0;
}
利用迭代器删除元素
对于关联容器 ,删除当前的迭代器仅仅会使当前迭代器失效。因为内部使用红黑树实现,所以只要在 之前递增当前迭代器即可。 对于序列容器 ,删除当前迭代器会使后面所有元素迭代器都失效。因为内部是连续分配的内存,删除一个元素会使后面的所有元素向前移动一个位置。但 可以返回下一个有效的迭代器。 对于 ,它使用了不连续分配的内存,并且 也会返回下一个有效的迭代器,所以两种方法都可行。
using namespace std;
int main()
{
set<int> st = {1, 2, 3, 4, 5, 6};
for(set<int>::iterator iter=st.begin(); iter!=st.end(); )
{
if(*iter == 3)
{
st.erase(iter++); // 传给 erase 的是 iter 的一个副本
}
else {
iter++;
}
}
vector<int> g = {1, 2, 3, 4, 5, 6};
for(vector<int>::iterator iter=g.begin(); iter!=g.end(); )
{
if(*iter == 3)
{
iter = g.erase(iter);
} else
{
iter++;
}
}
return 0;
}
在 ++ 的内存配置和释放操作中
运算分为两个阶段:1. 调用 ::operaotr new 分配一个对象的内存。2. 调用该对象的构造函数。 运算也分为两个阶段:1. 调用对象的析构函数。2. 调用 ::operaotr delete 释放内存。
而 将这两个阶段分开,用四个函数实现
负责内存配置 负责对象构造 负责对象析构 负责内存释放
为了提高内存管理效率,减少申请小内存产生的内存碎片, 设计了双层级配置器
当分配内存超过 时,使用第一层配置器,第一层配置器直接使用 进行内存的分配和释放。 当分配内存小于 时,使用第二层配置器,第二层配置器采用内存池技术、通过空闲链表来管理内存。
++11 特性
关键字,编译器可以通过初始值自动推导出类型,但不能用于函数传参和数组类型的推导。 关键字,在 ++ 中 被宏定义为 ,在遇到重载时可能有问题,而 一直是一个空指针,可以转换成其他任意类型的指针类型。 智能指针,新增了 等智能指针,用于解决内存管理。 初始化列表,可以使用初始化列表对类初始化。 右值引用,可以实现移动语句和完美转发,消除两个对象交互时不必要的拷贝,提高效率。 新增 容器 和 。 是封装固定大小数组的容器,支持传统数组的随机访问,还支持迭代器访问、获取容量等,并且不会退化成指针。 是多元组容器。
++11 可变参数模板
++ 11 的可变参数模板,可以表示任意数目,任意类型的参数,语法为在 或 后加上省略号。
using namespace std;
void print()
{
cout << endl;
}
template<class T, class... Args>
void print(T num, Args... rest)
{
cout << num << " ";
print(rest...);
}
int main()
{
print(1, 2, 3, 4, 5);
return 0;
}
++ 从源代码到可执行文件过程
预处理阶段:处理源代码中以 '#' 开头的预编译指令,生成 预编译(.i) 文件。 展开宏定义,将 删除 处理条件编译语句 处理 编译指令 删除注释 编译阶段:将预编译文件转化成相应的汇编码,生成 汇编(.s) 文件。 词法分析:从左到右将字符一个个的读入程序,分割成一个个记号 语法分析:将产生的记号进行语法分析,构成出语法树,此时很多运算符的优先级和含义就已经确定了。 语义分析:检查该语句是否有意义。比如两个指针相乘、是否有除 操作。 汇编阶段:将汇编文件转化成机械码,生成 可重定位目标(.o) 文件。 链接阶段:将多个目标文件及所需要的库链接成最终的 可执行(.out/.exe) 文件。 静态链接:在生成可执行文件的时候,把所有需要的函数的二进制代码都包含到可执行文件中去。 动态链接:在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去执行内存中已经加载的可执行代码,最终达到运行时连接的目的。
引用头文件顺序 以及 尖括号和双引号区别
如果在 中引用了 中的定义,而不想在 中引用 ,那么可以在 里面先引用 ,在引用 即可。
// head.h 内容
struct Node {
int a, b;
};
// main.h 内容
Node node
/// main.cpp 错误写法
int main() {
return 0;
}
// main.cpp 正确写法
int main() {
return 0;
}
使用尖括号和双引号的区别在于:编译器预处理阶段寻找头文件的路径顺序不一样。
使用双引号的查找顺序 当前头文件目录 编译器设置的头文件路径 系统变量指定的头文件路径 使用尖括号的查找顺序 编译器设置的头文件路径 系统变量指定的头文件路径
内存泄漏
内存泄漏是由于疏忽或错误操作使程序未能释放掉不再使用的内存的情况,并不是物理上的消失,而是程序分配某段内存后,由于设计错误,失去了对该段内存的控制,从而造成内存的浪费。
内存泄漏的情况:
程序通过 等内存申请操作后,在用完后没有调用 删除掉,那么此后这段内存不会再被使用,造成内存泄漏。 程序使用系统分配的资源而没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低。 基类析构函数没有定义成虚函数,那么子类的析构函数不会被调用,子类的内存没有被释放,造成内存泄漏。
内存溢出
程序申请内存的时候,超出了系统实际分配给你的空间,此时系统无法完成满足你的需求,就会发生内存溢出。
内存溢出的情况:
内存中加载的数据量过于庞大。 代码中存在死循环或者递归过深导致栈溢出。 内存泄漏导致内存溢出。
段错误
段错误就是访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的。
段错误的原因:
使用了野指针 试图对只读空间进行写操作 访问了不存在或系统保护的内存空间 四种 转换
四种 转换为:。
用于将 变量转换成非 。 用于各种隐式转换,比如非 转 、 转指针等,能用于多态中向上转化,向下转化能成功但结果未知。 动态类型转换,只能用于含有虚函数的类,用于类层次间的向上和向下转化,只能转指针或引用。向下转换失败时,对于指针返回 ,对于引用抛出异常。 几乎什么都可以转,但可能会出问题。
语言的强制转化看起来功能强大,但转化不够准确,不能进行错误检查,不安全。
四种智能指针
四种智能指针:,第一种已经被 ++11 弃用。
(替换 ) 实现独占式拥有概念。保证同一时间只有一个 指针指向某个内存,对于避免内存泄漏很有用。 实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在 "最后一个引用被销毁" 时释放。 两个对象互相使用一个 成员变量指向对方,会造成循环引用,使得两个对象都不会自动释放,造成内存泄漏。如下例子,可以看出析构函数没有被调用。为了解决这个问题,引入了 。
using namespace std;
class A;
class B;
class A {
public:
A(){
cout << "A Created" << endl;
}
~A(){
cout << "A Destroyed" << endl;
}
shared_ptr ptr;
};
class B {
public:
B(){
cout << "B Created" << endl;
}
~B(){
cout << "B Destroyed" << endl;
}
shared_ptr ptr;
};
int main(){
shared_ptr pt1(new A());
shared_ptr pt2(new B());
pt1->ptr = pt2;
pt2->ptr = pt1;
cout << "use of pt1: " << pt1.use_count() << endl;
cout << "use of pt2: " << pt2.use_count() << endl;
return 0;
}
/*
A Created
B Created
use of pt1: 2
use of pt2: 2
*/
是一种不控制对象生命周期的智能指针,它与一个 绑定,却不参与引用计数。一旦最后一个 销毁,对象就会释放。 作用是在需要的时候变出一个 ,其他时候不干扰 的引用计数。 没有 * 和 -> 符号,需要用 获得 ,进而使用对象。 利用 可以消除上面的循环引用,例子如下。
using namespace std;
class A;
class B;
class A {
public:
A() {
cout << "A Created" << endl;
}
~A() {
cout << "A Destroyed" << endl;
}
weak_ptr ptr;
};
class B {
public:
B() {
cout << "B Created" << endl;
}
~B() {
cout << "B Destroyed" << endl;
}
weak_ptr ptr;
};
int main() {
shared_ptr pt1(new A());
shared_ptr pt2(new B());
pt1->ptr = pt2;
pt2->ptr = pt1;
cout << "use of pt1: " << pt1.use_count() << endl;
cout << "use of pt2: " << pt2.use_count() << endl;
return 0;
}
/*
A Created
B Created
use of pt1: 1
use of pt2: 1
B Destroyed
A Destroye
*/
因为存在这种情况:申请的空间在函数结束后忘记释放,造成内存泄漏。使用智能指针可以很大程度的避免这个问题,因为智能指针是一个类,超出类的作用范围后,类会调用析构函数释放资源,所以智能指针的作用原理就是在函数结束后自动释放内存空间。