目录
一、三块核心内容
1.1 进程的虚拟地址空间内存划分和布局
任何的编程语言编译所产生的只有 指令和数据。
CPU运行程序,需要将.exe/.out文件加载到内存中,但不是直接加载到实际的物理内存中,而是加载到进程的虚拟地址空间。x86 32位Linux环境下,Linux内核会给当前进程分配一块2^32大小的空间,大小为4G。而虚拟地址空间不是物理内存,本质底层就是内核所创建的一系列数据结构。
在代码中定义的变量分别储存在虚拟空间的那个位置?
还需注意,每一个进程的用户空间是私有的,而内核空间是共享的。
1.2 函数的调用堆栈详细过程
原理:函数调用堆栈是由操作系统管理的一块内存区域,它用于存储函数调用和返回的过程中的临时数据。在函数调用过程中,程序会将函数的参数和返回地址压入堆栈中,然后跳转到函数的入口地址执行函数代码。在函数返回过程中,程序会从堆栈中弹出返回地址和临时数据,然后跳转到返回地址执行函数调用后的代码。
栈指针(Stack Pointer):指向堆栈顶部的指针。
帧指针(Frame Pointer):指向当前函数的帧的指针。
在x86结构里,负责存放栈底指针的寄存器是ebp,负责存放栈顶指针的寄存器是esp
详细步骤:
1.将形参压入堆栈中。(参数的压入顺序是从右到左)
2.将返回地址压入堆栈中。(返回地址是指函数返回后跳转的地址,它通常是函数调用指令的下一条指令地址)
3.跳转到函数入口地址执行函数代码。(在x86架构中,函数入口地址是通过CALL指令设置的)
4.收回函数的内存空间(恢复堆栈指针的值)
5.弹出返回地址
6.恢复堆栈指针,收回形参空间,跳转到返回地址执行函数调用后的代码。
函数调用过程示意图如下:
1.3 程序编译链接原理
1.预处理:处理以“#”开头的预处理指令,如#include,#define 等(#pragma lib/link 等命令则是链接时使用)。预处理器会将头文件包含进来,展开宏定义等,生成一个经过预处理的源代码文件。
2.编译:在这个阶段,编译器将预处理后的源代码翻译成中间代码(通常是汇编代码),这个中间代码仍然是针对特定的硬件平台的抽象代码。
3.汇编:分为两种x86和AT&T;将汇编代码转变成机器可以执行的指令,汇编后生成二进制可重定位的目标文件.o/.obj,可以通过objdump命令来查看.o或.obj文件的相关信息(objdump -t main.o)。
.o文件的格式组成(elf文件头–.text–.data–.bss–.symbal–.section table)
4.链接(.o → a.out / .obj→.exe):
(1)所有.o文件段的合并, 也就是main.o和sum.o的.text、.data等段合并到一起。
(2)符号解析,可以理解为: 所有对符合引用,都要找到该符号定义的地方;也就是链接器寻找main.o文件中UND(调用)符号定义的地方,如果找遍了所有地方都没有找到,那么链接器就会报错:符号未定义!,或者是在多个地方都找到了相同的符号定义,那么也会报错:符号重定义!
(3)符号的重定向,给所有的符号分配虚拟地址。
二、C++学习还必须掌握的-基础知识
2.1 malloc/free,new/delete
malloc/free和new/delete一般情况下的使用:
// malloc/free
int *p = (int* ) malloc (sizeof (int) );
if (p == nullptr) return -1;
*p = 20;
free(p);
// new/delete
int *pl = new int (20);
delete p1;
malloc/free和new/delete的区别
new 先开辟内存,再调用构造函数; delete先析构函数,再释放内存。
- malloc和free是函数,new和delete是操作符
- malloc/free只会开辟,释放空间;而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可。
- malloc开辟内存失败,是通过返回值和nullptr做比较;而new开辟内存失败,是通过抛出bad_alloc类型的异常来判断的。
什么是内存泄漏?
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
new和delete能混用吗?
new delete new[ ] delete[ ]
对于普通的编译器内置类型int等(只有内存开辟,无析构),可以进行混用。
自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组的时候,会多开辟4个字节,记录对象的个数,故不能混用。
C++为什么区分单个元素和数组的内存分配和释放呢?
数组内存释放会多次调用析构函数。
2.2 引用与指针
指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个对象;
引用:和原来的变量实质上是同一个东西,相当于是原变量的一个别名。可以看作一种更安全的指针。
int a=1;int *p=&a;
int a=1;int &b=a;
区别:
1.引用是必须初始化的,指针可以不初始化。
2.引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针。
3.指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了,从一而终。
4.sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
联系:
定义一个引用变量,和定义一个指针变量,其汇编指令是一模一样的;通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的。
2.3 C++传递方式
2.3.1 传值(Pass by Value)
void foo(int x) { };
当函数参数是以传值的方式传递时,实际上传递的是参数的副本,而不是参数本身。
优点:由于是操作副本,所以原始数据不会被修改,这在很多情况下是预期的行为。
缺点:如果参数是大型结构或类实例,那么复制整个对象可能会导致性能损耗。
void foo(int x) { //此处传值
x = 30; // 只修改了 x 的副本,不会影响到原始变量
}
int main() {
int a = 10;
foo(a); // a 的值仍然是 10
}
2.3.2 传引用(Pass by Reference)nice
void foo(int& x) { };
传引用意味着传递的是参数的引用(实际上是内存地址),而非其副本。
优点:可以直接修改原始数据,对于大型数据结构或对象,传引用避免了复制的开销,可以提高程序的性能。
缺点:由于可以修改原始数据,如果不小心处理,可能会导致错误或不可预期的结果。
void foo(int& x) {
x = 30; // 修改了引用的实际对象,即原始变量
}
int main() {
int a = 10;
foo(a); // 现在 a 的值变为了 30
}
2.3.3 传常引用(Pass by Constant Reference)
结合了传值和传引用的优点:它通过引用传递参数以提高效率,同时通过使参数为 const,防止函数修改参数值。
void foo(const int& x) {
// x = 30; // 这行代码会导致编译错误,因为 x 是 const 的
}
int main() {
int a = 10;
foo(a);
// a 的值仍然是 10,foo 函数不能修改 x
}
2.4 Const,Const与指针(引用)的结合
const的语义为只读。修饰的值不能改变;必须在定义时就给它赋予初值,无论是const还是非const变量都可以对其进行初始化;初始化后不能再作为左值。
c和c++中const的区别是什么?
const的编译方式不同。c中const就是当作一个变量来编译生成指令的;
C++中,所有出现const常量名字的地方,都被常量的初始化替换了! ! !
const int a = 20;
int array [a]= { }; // C中会报错,以变量来生成指令。 C++中可以执行
int *p = (int* ) &a;
*p = 30;
printf ("%d %d %d \n",20,*p,20 ) ; // C中输出为 30 30 30,C++中输出为 20 30 20
const修饰的量常出现的错误是:
- 常量不能再作为左值 ⬅ 直接修改常量的值
- 不能把常量的地址泄露给一个普通的指针或者普通的引用变量 ⬅ 可以间接修改常量的值大
const在C++中的语言规范:
- const修饰的是离它最近的类型
- const如果右边没有指针*的话, const是不参与类型的
const int *p = &a; // *p = 20, p = & b
可以任意指向不同的int类型的内存,但是不能通过指针间接修改指向的内存的值。
int *const p = &a;// p = &b;
常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。
总结const和指针的类型转换公式:
int* ⬅ const in* 是错误的!
!const int⬅int 是可以的!
int⬅const int* 是错误的!
const int ⬅ int* 是错误的!
2.5 函数重载
什么是函数重载?
一组函数,函数名相同,参数列表的个数或者类型不同,那么这一组函数就称作-函数重载。
—组函数要称得上重载,一定先是处在同一个作用域当中的。
函数重载需要注意些什么?
1.C++为什么支持函数重载,c语言不支持函数重载?
C++代码产生函数符号的时候,函数名+参数列表类型组成的! 如:sum_int_int
c代码产生函数符号的时候,函数名来决定! 如:sum
2.添加const不影响函数名,仅仅返回值不同不叫重载。
C++和C语言代码之间如何协调?
1.c调用C++:无法直接调用了!怎么办?
把c++源码括在extern “c”{ … }
2.C++调用c代码:无法直接调用了!怎么办?
把c函数的声明括在extern “c”{ … }
2.6 inline内联函数
内联函数使用: inline关键字 + 函数。
inline内联函数 和 普通函数的区别???
内联函数:在编译过程中,就没有函数的调用开销了,在函数的调用点直接把函数的代码进行展开处理了
Iinline函数不再生成相应的函数符号。
普通函数:有标准的函数调用过程,参数压栈,函数栈帧的开辟和回退过程,有函数调用的开销。
注意:
inline只是建议编译器把这个函数处理成内联函数,但是不是所有的inline都会被编译器处理成内联函数。
debug版本上,inline是不起作用的;inline只有在release版本下才能出现
2.7 参数带默认值的函数
1.给默认值的时候,从右向左给。
2.定义处可以给形参默认值,声明也可以给形参默认值。
3,形参给默认值的时候,不管是定义处给,还是声明处给,形参默认值只能出现一次。
三、C++面向对象
3.1 类和对象、this指针
3.1.1 类和对象
类是实体的抽象类型
实体包括属性和行为→ ADT(abstract data type )
对象 ⬅ (实例化) 类 (属性→成员变量 ,行为→成员方法)
ooP语言的四大特征是什么?
抽象、封装/隐藏、继承、多态
类的特点:
在类中,属性一般都是私有变量,通过给外部提供公有的成员方法,来访问私有的属性(如给成员变量提供一个getXXX或者setXXX方法)
类通过实例化来创建对象,每个类可以创建无数个对象,每个对象都有自己的成员变量,但是他们共享成员方法。
类的成员方法一经编译,所有的方法参数,都会加一个this指针,接收调用该方法的对象的地址。
3.1.2 this指针
this指针的主要用途包括:
1.访问对象的成员变量和成员函数。
2.实现方法链(method chaining)。
3.用于区分成员变量和函数参数。
4.返回对象本身的指针。
3.2 构造函数和析构函数
构造函数(初始化) :
1.语法:类名(){ … }
2.构造函数可以有参数,因此可以发生重载。
3.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。
析构函数(释放内存):
1.语法:~类名(){ … }
2.析构函数不可以有参数,因此不可以发生重载。
3.程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
构造函数的初始化列表:可以指定当前对象成员变量(私有变量中创建另一个类的对象,具有被包含关系)的初始化方式
在构造函数后接:进行初始化。
3.3 对象的浅拷贝和深拷贝
浅拷贝:指复制对象时,只复制对象的指针,而不复制对象本身。这意味着新对象和原对象将共享相同的内存地址。如果原对象发生变化,新对象也会受到影响。
深拷贝:指复制对象时,先创建足够的空间,再把蓝本的内容拷贝过去。这意味着新对象和原对象不共享相同的内存地址。如果原对象发生变化,新对象不会受到影响。
编译器默认为浅拷贝,深拷贝需要自己进行编写。
示例(顺序栈-深拷贝)
SeqStack(const SeqStack& src) //拷贝构造函数,深拷贝
{
_pstack = new int [src._size];
for(int i = 0; i <= src._top; ++i){
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
3.4类的各种成员方法以及区别
3.4.1 普通的成员方法(编译器会添加一个this形参变量)
属于类的作用域。
调用该方法时,需要依赖一个对象(常对象是无法调用的)。
可以任意访问对象的私有成员,protected继承 public private。
3.4.2 static静态成员方法(不会生成this形参)
属于类的作用域。
用类名作用域来调用方法。
可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其它的static静态成员)。
注意:static成员变量必须在类外定义且初始化,定义时不添加static关键字
class A
{
private:
//声明
static int count;
};
//定义
int A::count = 0;