C++学习(1)基础八股文
1、关键字与运算符
1.1 define 和 typedef 及 inline 的区别
define
a. 只是ᓌ单的字符串替换,没有类型检查;
b. 是在编译的预处理ᴤ段起作⽤;
c. 可以⽤来防⽌头⽂件重复引⽤;
d. 不分配内存,给出的是⽴即数,有多少次使⽤就进⾏多少次替换。
typedef
a. 有对应的数据类型,是要进⾏判断的;
b. 是在编译、运⾏的时候起作⽤;
c. 在静态存储区中分配空间,在程序运⾏过程中内存中只有⼀个拷⻉;
inline
a. inline是先将内联函数编译完成⽣成了函数体直接插⼊被调⽤的地⽅,减少了ܴ栈,跳转和返回的操作。没有普通函数调⽤时额外外开销;
b. 内联函数是⼀种特ྛ的函数,会进⾏类型检查;
c. 对编译器的⼀种请求,编译器有可能绝这种请求。
C++中inline编译限制:
- 不能存在任何形式的循环语句;
- 不能存在过多的条件判断语句;
- 函数体不能过于ଲ⼤;
- 内联函数声明必须在调⽤语句之前。
1.2 new 和 malloc
a、new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL;
b、使⽤new操作符申请内存分配时⽆须指定内存块的⼤⼩,⽽malloc则需要显式地指出所需内存的尺寸;
c、opeartor new /operator delete可以被重载,⽽malloc/free并不允许重载;
d、new/delete会调⽤对象的构造函数/析构函数以完成对象的构造/析构。⽽malloc则不会 ;
e、malloc与free是C++/C语⾔的标准库函数,new/delete是C++的运算符 ;
f、new操作符从⾃由存储区上为对象动态分配内存空间,⽽malloc函数从堆上动态分配内存。
new/delete | malloc/free | |
---|---|---|
本质属性 | 运算符 | crt函数 |
内存分配大小 | 自动计算 | 手动计算 |
类型安全 | 是 | 不是 |
两者关系 | new封装了malloc | |
其他特点 | 会调用构造和析构函数、失败抛出bac_alloc异常、返回具体类型指针 | 失败返回null、返回void指针 |
2、C++三大特性
2.1 访问权限
- C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符;
- 在类的内部(定义类的代码内部),⽆论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制;
- 在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public属性的成员,不能访问 private、protected 属性的成员;
- ⽆论共有继承、私有和保护继承,私有成员不能被“派⽣类”访问,基类中的共有和保护成员能被“派⽣类”访问;
- 对于共有继承,只有基类中的共有成员能被“派⽣类对象”访问,保护和私有成员不能被“派⽣类对象”访问。对于私有和保护继承,基类中的所有成员不能被“派⽣类对象”访问。
2.2 封装
定义:
- 让某种类型对象获得另⼀个类型对象的属性和⽅法。
功能:
- 它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对这些功能进⾏扩展。
常⻅的继承有三种⽅式:
a. 实现继承:指使⽤基类的属性和⽅法⽽⽆需᷐外编码的能⼒;
b. 接⼝继承:指仅使⽤属性和⽅法的名称、但是⼦类必须提供实现的能⼒;
c. 可视继承继承:指⼦窗体(类)使⽤基窗体(类)的外观和实现代码的能⼒。
2.3 封装
定义:
- 数据和代码捆绑在⼀起,避免外界干扰和不确定性访问;
功能:
- 把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信的进⾏信息隐藏,例如:将公共的数据或⽅法使⽤public修饰,⽽不希望被访问的数据或⽅法采⽤private修饰。
2.4 多态
定义:
- 同⼀事物表现出不同事物的能⼒,即向不同对象发送同⼀消息,不同的对象在接收时会产⽣不同的⾏为(重载实现编译时多态,虚函数实现运⾏时多态)。
功能:
- 多态性是允许你将⽗对象设置成为和⼀个或更多的他的⼦对象相等的技术,赋值之后,⽗对象就可以根据当前赋值给它的⼦对象的特性以不同的⽅式运作-->⼦类类型的指针赋值给父类类型的指。
实现多态有两种⽅式
a. 重写(override)(动态多态): 是指⼦类重新定义⽗类的虚函数的做法
b. 重载(overload)(静态多态): 是指允许存在多个同名函数,⽽这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)
2.4.1 静态多态——重载
a. 不能通过访问权限、返回类型、抛出的异常进⾏重载
b. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不⼀样)
c. ⽅法的异常类型和数⽬不会对重载造成响
2.4.2 动态多态——虚函数
- 当基类希望派⽣类定义适合自己版本,就将这些函数声明成虚函数(virtual),虚函数依赖虚函数表⼯作,表来保存虚函数地址,当我们⽤基类指针指向派⽣类时,虚表指针指向派⽣类的虚函数表,这个机制可以保证派⽣类中的虚函数被调⽤到。
a. 动态绑定
- 虚函数是动态绑定的,使⽤虚函数的指针和引⽤能够正确找到实际类的对应函数,⽽不是执⾏定义类的函数,这是虚函数的基本功能,所对应的函数或属性依赖于对象的动态类型,发⽣在运⾏期。
b. 多态(不同继承关系的类对象,调⽤同⼀函数产⽣不同⾏为)
- 调⽤函数的对象必须是指针或者引⽤;
- 被调⽤的函数必须是虚函数(virtual),且完成了虚函数的重写(派⽣类中有⼀个跟基类的完全相同虚函数)
c. 构造函数和析构函数
- 构造函数不能是虚函数,因为虚函数是通过虚函数表,而指向虚函数表的指针是在创建对象后才有,即等构造函数执行完才初始化;
- 析构函数可以是虚函数,且析构函数尽量是虚函数。
d. 工作方式
- 虚函数依赖虚函数表⼯作,表用来保存虚函数地址,当我们⽤基类指针指向派⽣类时,虚表指针vptr指向派⽣类的虚函数表。 这个机制可以保证派⽣类中的虚函数被调⽤到。
e.纯虚函数
- 纯虚函数是在基类中声明的虚函数,它可以在基类中有定义,而且派生类必须定义自己的实现方法。基类不能生成对象,可以使用指针或者引用派生类对象。基类不在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0
f. inline, static三种函数᮷不能带有virtual关键字
- inline是在编译时展开,必须要有实体,但虚函数的多态特性需要在运⾏时根据对象类型才知道调⽤哪个虚函数,所以没法在编译时进⾏内联函数展开。
- static属于class⾃⼰的类相关,没有this指针。virtual函数⼀定要通过对象来调⽤,有隐藏的this指针,实例相关。
2.5 虚继承
- 解决多继承时的命名冲突和ٞ֟数据问题:C++提出了虚继承,使得在派⽣类中只保留⼀份间接基类的成员。其中多继承是指从多个直接基类中产⽣派⽣类的能⼒,多继承的派⽣类继承了所有⽗类的成员。
- 虚继承的是让某个类共享他的基类:被共享的基类就称为虚基类,在这种机制下,不论虚基类在继承体系中出现了多少次,在派⽣类中都只包含⼀份虚基类的成员,可参考C++标准库中的 iostream 类,从 istream 和 ostream 直接继ಥ⽽来,⽽ istream 和 ostream ⼜都继ಥ⾃⼀个共同的名为 base_ios 的类。
- ⼀般只有在⽐较简单和不易出现⼆义性或者实在必要情况下才使⽤多继承,能⽤单⼀继承解决问题就不要⽤多继承。
2.6 空类
- 空类的⼤⼩不为0,是1,确保两个不同对象的地址不同,类的实例化是在内存中分配⼀块地址,每个实例在内存中都有独⼀⽆⼆的⼆地址。
class A{ virtual void f(){} };
class B:public A{}
- 类A和类B都不是空类,其sizeof都是4,因为它们都具有虚函数表的地址。
class A{};
class B:public virtual A{};
- A是空类,其⼤⼩为1;B不是空类,其⼤⼩为4.因为含有指向虚基类的指针。
class Father1{}; class Father2{};
class Child:Father1, Father2{};
- 多重继承的空类的⼤⼩是1。
3、智能指针
智能指针其作⽤是管理⼀个指针,避免程序员申请的空间在函数结束时忘记释放,造成内存泄漏这种情况的发⽣。使⽤智能指针可以很⼤程度上的避免这个问题,因为智能指针就是⼀个类,当超出了类的作⽤域是,类会⾃动调⽤析构函数,析构函数会⾃动释放资源。所以智能指针的作⽤原理就是在函数结束时⾃动释放内存空间,不需要⼿动释放内存空间。
常⽤接⼝:
T* get();
T& operator*();
T* operator->();
T& operator=(const T& val);
T* release();
void reset (T* ptr = nullptr);
- T 是模板参数, 也就是传⼊的类型;
- get()⽤来获取auto_ptr封装在内部的指针, 也就是获取原⽣指针;
- operator() 重载 ,operator->() 重载了->, operator=()重载了=;
- realease()将 auto_ptr封装在内部的指针置为nullptr, 但并不会破坏指针所指向的内容, 函数返回的是内部指针置空之前的值;直接释放封装的内部指针所指向的内存, 如果指定了 ptr的值, 则将内部指针初始化为该值 (否则将其设置为nullptr)
3.1 auto_ptr(C++98 的⽅案,C11 已抛弃)采⽤所有权模式。
auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错.
此时不会报错,p2剥夺了 p1 的所有权,但是当程序运⾏时访问 p1 将会报错。所以 auto_ptr的缺点是:存在潜在的内存崩溃问题.
3.2、unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同⼀时间内只有⼀个智能指针可以指向该对象。它对于避免资源泄露特别有⽤。
采⽤所有权模式,还是上⾯那个例⼦
unique_ptr<string> p3 (new string (auto));
unique_ptr<string> p4;
p4 = p3;//此时会报错
编译器认为p4=p3⾮法,避免了 p3不再指向有效数据的问题。因此,unique_ptr ⽐ auto_ptr更安全。
3.3 shared_ptr(共享型,强引⽤)
shared_ptr实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在 "最后⼀个引⽤被销毁" 时候释放。从名字 share 就可以看出了资源可以被多个指针共享,它使⽤计数机制来表明资源被⼏个指针共享。
可以通过成员函数 use_count() 来查看资源的所有者个数,除了可以通过 new 来构造,还可以通过传⼊auto_ptr,unique_ptr,weak_ptr 来构造。当我们调⽤ release() 时,当前指针会释放资源所有权,计数减⼀。当计数等于 0 时,资源会被释放。
3.4 weak_ptr(弱引⽤)
weak_ptr是⼀种不控制对象⽣命周期的智能指针,它指向⼀个shared_ptr 管理的对象。进⾏该对象的内存管理的是那个强引⽤的 shared_ptr。
weak_ptr只是提供了对管理对象的⼀个访问⼿段。weak_ptr 设计的⽬的是为配合shared_ptr⽽引⼊的⼀种智能指针来协助 shared_ptr ⼯作,它只可以从⼀个 shared_ptr或另⼀个 weak_ptr 对象构造,,它的构造和析构不会引起引⽤记数的增加或减少。
weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 shared_ptr 之间可以相互转化,shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr
当两个智能指针都是shared_ptr类型的时候,析构时两个资源引⽤计数会减⼀,但是两者引⽤计数还是为 1,导致跳出函数时资源没有被释放(析构函数没有被调⽤),解决办法:把其中⼀个改为weak_ptr就可以。
4、C++强制类型转换
强制类型转换是有一定风险的,有的转换并不一定安全,例如把int整形数值转换成一个指针类型,把基类指针转换成派生类指针的时候有可能会失败,把一种函数指针转换成另一种函数指针可能会出现不匹配的情况,把常量指针转换成非常量指针可能会导致原始常量被破坏等等并不是很安全的。C++ 引入新的强制类型转换机制,主要是为了克服C语言强制类型转换的以下三个缺点。
- 没有从形式上体现转换功能和风险的不同。
- 将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。
- 难以在程序中寻找到底什么地方进行了强制类型转换。
关键字:static_cast、dynamic_cast、reinterpret_cast和 const_cast
4.1 static_cast
static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。
static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。
用法示例如下:
class A
{
public:
operator int() { return 1; }
operator char*() { return NULL; }
};
A a;
int n;
const char* p = "This is a str for static_cast";
n = static_cast <int> (3.14); // n 的值变为 3
n = static_cast <int> (a); // 调用 a.operator int, n 的值变为 1
p = static_cast <char*> (a); // 调用 a.operator char*,p 的值变为 NULL
// n = static_cast <int> (p); // 编译错误,static_cast不能将指针转换成整型
// p = static_cast <char*> (n); // 编译错误,static_cast 不能将整型转换成指针
4.2 dynamic_cast
用 reinterpret_cast 可以将多态基类(包含虚函数的基类)的指针强制转换为派生类的指针,但是这种转换不检查安全性,即不检查转换后的指针是否确实指向一个派生类对象。dynamic_cast专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 NULL 指针。
dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。
dynamic_cast 示例程序如下:
class Base
{
//有虚函数,因此是多态基类
public:
virtual ~Base() {}
};
Base b;
Derived d;
Derived* pd;
pd = reinterpret_cast <Derived*> (&b);
if (pd == NULL)
//此处pd不会为 NULL。reinterpret_cast不检查安全性,总是进行转换
cout << "unsafe reinterpret_cast" << endl; //不会执行
pd = dynamic_cast <Derived*> (&b);
if (pd == NULL) //结果会是NULL,因为 &b 不指向派生类对象,此转换不安全
cout << "unsafe dynamic_cast1" << endl; //会执行
pd = dynamic_cast <Derived*> (&d); //安全的转换
if (pd == NULL) //此处 pd 不会为 NULL
cout << "unsafe dynamic_cast2" << endl; //不会执行
4.3 reinterpret_cast
reinterpret_cast 用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换,reinterpret_cast 转换时,执行的过程是逐个比特复制的操作。
这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。例如,程序员执意要把一个 int* 指针、函数指针或其他类型的指针转换成 string* 类型的指针也是可以的,至于以后用转换后的指针调用 string 类的成员函数引发错误,程序员也只能自行承担查找错误的烦琐工作:(C++ 标准不允许将函数指针转换成对象指针,但有些编译器,如 Visual Studio 2010,则支持这种转换)。
reinterpret_cast 用法示例如下:
class A
{
public:
int i;
int j;
A(int n) :i(n), j(n) { }
};
A a(100);
int &r = reinterpret_cast<int&>(a); // 强行让 r 引用 a
r = 200; // 把 a.i 变成了 200
cout << a.i << "," << a.j << endl; // 输出 200,100
int n = 300;
A *pa = reinterpret_cast<A*> (&n); // 强行让 pa 指向 n
pa->i = 400; // n 变成 400
pa->j = 500; // 此条语句不安全,很可能导致程序崩溃
cout << n << endl; // 输出 400
long long la = 0x12345678abcdLL;
pa = reinterpret_cast<A*>(la); // la太长,只取低32位0x5678abcd拷贝给pa
unsigned int u = reinterpret_cast<unsigned int>(pa);// pa逐个比特拷贝到u
cout << hex << u << endl; // 输出 5678abcd
typedef void(*PF1) (int);
typedef int(*PF2) (int, char *);
PF1 pf1 = nullptr;
PF2 pf2;
pf2 = reinterpret_cast<PF2>(pf1); // 两个不同类型的函数指针之间可以互相转换
4.4 const_cast
const_cast 运算符仅用于进行去除 const 属性的转换,它也是四个强制类型转换运算符中唯一能够去除 const 属性的运算符。
将 const 引用转换为同类型的非 const 引用,将 const 指针转换为同类型的非 const 指针时可以使用 const_cast 运算符。例如:
const string s = "Inception";
string& p = const_cast <string&> (s);
string* ps = const_cast <string*> (&s); // &s 的类型是 const string*
5、构造函数
类的对象被创建时,编译系统为对象分配内存空间,并⾃动调⽤构造函数,由构造函数完成成员的初始化⼯作。
即构造函数的作⽤:初始化对象的数据成员。
-
⽆参数构造函数:即默认构造函数,如果没有明确写出⽆参数构造函数,编译器会⾃动⽣成默认的⽆参数构造函数,函数为空,什么也不做,如果不想使⽤⾃动⽣成的⽆参构造函数,必需要⾃⼰显示写出⼀个⽆参构造函数。
-
⼀般构造函数:也称᯿载构造函数,⼀般构造函数可以有各种参数形式,⼀个类可以有多个⼀般构造函数,前提是参数的个数或者类型不同,创建对象时根据传⼊参数不同调⽤不同的构造函数。
-
拷⻉构造函数:拷⻉构造函数的函数参数为对象本身的引⽤,⽤于根据⼀个已存在的对象复制出⼀个新的该类的对象,⼀般在函数中会将已存在的对象的数据成员的值⼀⼀复制到新创建的对象中。如果没有显示的写拷⻉构造函数,则系统会默认创建⼀个拷⻉构造函数,但当类中有指针成员时,最好不要使⽤编译器提供的默认的拷⻉构造函数,最好⾃⼰定义并且在函数中执⾏深拷⻉。
注意:拷⻉构造函数必需时引⽤传递,为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。 -
类型转换构造函数:根据⼀个指定类型的对象创建⼀个本类的对象,也可以算是⼀般构造函数的⼀种,这⾥提出来,是想说有的时候不允许默认转换的话,要记得将其声明为 explict 的,来阻⽌⼀些隐式转换的发⽣。
-
赋值运算符的重载:注意,这个类似拷⻉构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会⽣成默认的赋值运算符,做⼀些基本的拷⻉⼯作。
6 C++内存模型
6.1 内存对齐
内存对⻬的规则:
- 对于结构体中的各个成员,第⼀个成员位于偏移为 0 的位置,以后的每个数据成员的偏移量必须是 min(#pragma pack() 制定的数,数据成员本身⻓度) 的倍数。
- 在所有的数据成员完成各⾃对⻬之后,结构体或联合体本身也要进⾏对⻬,整体⻓度是min(#pragma pack()制定的数,⻓度最⻓的数据成员的⻓度) 的倍数。
那么内存对⻬的作⽤: - 经过内存对⻬之后,CPU 的内存访问速度⼤⼤提升。因为 CPU 把内存当成是⼀块⼀块的,块的⼤⼩可以是 2,4,8,16 个字节,因此 CPU 在读取内存的时候是⼀块⼀块进⾏读取的,块的⼤⼩称为内存读取粒度。⽐如说 CPU 要读取⼀个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进⾏处理即可。
- 如果数据是从 1 字节开始的,就⾸先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进⼊寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4字节的数据进⼊寄存器,所以说,当内存没有对⻬时,寄存器进⾏了很多额外的操作,⼤⼤降低了 CPU 的性能。
- 另外,还有⼀个就是,有的 CPU 遇到未进⾏内存对⻬的处理直接拒绝处理,不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。所以内存对⻬还有利于平台移植。
6.2 内存泄漏
定义:内存泄漏简单的说就是申请了⼀块内存空间,使⽤完毕后没有释放掉。 它的⼀般表现⽅式是程序运⾏时间越⻓,占⽤内存越多,最终⽤尽全部内存,整个系统崩溃。由程序申请的⼀块内存,且没有任何⼀个指针指向它,那么这块内存就泄漏了。
如何检测内存泄漏:
- ⾸先可以通过观察猜测是否可能发⽣内存泄漏,Linux 中使⽤ swap 命令观察还有多少可⽤的交换空间,在⼀两分钟内键⼊该命令三到四次,看看可⽤的交换区是否在减少。
- 还可以使⽤ 其他⼀些 /usr/bin/stat ⼯具如 netstat、vmstat 等。如发现波段有内存被分配且从不释放,⼀个可能的解释就是有个进程出现了内存泄漏。
- 当然也有⽤于内存调试,内存泄漏检测以及性能分析的软件开发⼯具 valgrind 这样的⼯具来进⾏内存泄漏的检测。
6.3 内存分配
- 栈:由编译器管理分配和回收,存放局部变量和函数参数。
- 堆:由程序员管理,需要手动 new malloc delete free 进⾏分配和回收,空间较大,但可能会出现内存泄漏和空闲碎⽚的情况。
- 全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
- 常量存储区:存储常量,⼀般不允许修改。
- 代码区:存放程序的⼆进制代码。
6.4 堆栈空间
栈
- 由编译器进⾏管理,在需要时由编译器⾃动分配空间,在不需要时候⾃动回收空间,⼀般保存的是局部变量和函数参数等。
- 连续的内存空间,在函数调⽤的时候,⾸先⼊栈的主函数的下⼀条可执⾏指令的地址,然后是函数的各个参数。
- ⼤多数编译器中,参数是从右向左⼊栈(原因在于采⽤这种顺序,是为了让程序员在使⽤C/C++的“函数参数⻓度可变”这个特性时更⽅便。如果是从左向右压栈,第⼀个参数(即描述可变参数表各变ᰁ类型的那个参数)将被放在栈底,由于可变参的函数第⼀步就需要解析可变参数表的各参数类型,即第⼀步就需要得到上述参数,因此,将它放在栈底是很不⽅便的。)本次函数调⽤结束时,局部变ᰁ先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程序由该点继续运⾏,不会产⽣碎⽚。
- 栈是⾼地址向低地址扩展,栈低⾼地址,空间较⼩。
堆
- 由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,如果不进⾏回收的话,会造成内存泄漏的问题。
- 不连续的内存空间,实际上系统中有⼀个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第⼀个⼤于等于申请⼤⼩的空间分配给程序,⼀般在分配程序的时候,也会空间头部写⼊内存⼤⼩,⽅便 delete 回收空间⼤⼩。当然如果有剩余的,也会将剩余的插⼊到空闲链表中,这也是产⽣内存碎⽚的原因。
- 堆是低地址向⾼地址扩展,空间交⼤,较为灵活。
7 C++编译
7.1 预处理,编译,汇编,链接程序的区别
⼀段⾼级语⾔代码经过四个阶段的处理形成可执⾏的⽬标⼆进制代码。预处理器→编译器→汇编器→链接器:最难理解的是编译与汇编的区别。
这⾥采⽤《深⼊理解计算机系统》的说法。
- 预处理阶段:写好的⾼级语⾔的程序⽂本⽐如 hello.c,预处理器根据 #开头的命令,修改原始的程序,如#include<stdio.h> 将把系统中的头⽂件插⼊到程序⽂本中,通常是以 .i 结尾的⽂件。
- 编译阶段:编译器将 hello.i ⽂件翻译成⽂本⽂件 hello.s ,这个是汇编语⾔程序。⾼级语⾔是源程序。所以注意概念之间的区别。汇编语⾔程序是⼲嘛的?每条语句都以标准的⽂本格式确切描述⼀条低级机器语⾔指令。不同的⾼级语⾔翻译的汇编语⾔相同。
- 汇编阶段:汇编器将 hello.s 翻译成机器语⾔指令。把这些指令打包成可重定位⽬标程序,即.o⽂件。hello.o是⼀个⼆进制⽂件,它的字节码是机器语⾔指令,不再是字符。前⾯两个阶段都还有字符。
- 链接阶段:⽐如 hello 程序调⽤ printf 程序,它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于⼀个名叫 printf.o 的单独编译好的⽬标⽂件中,这个⽂件将以某种⽅式合并到 hello.o 中。链接器就负责这种合并。得到的是可执⾏⽬标⽂件。
7.2 动态编译与静态编译
- 静态编译:编译器在编译可执⾏⽂件时,把需要⽤到的对应动态链接库中的部分提取出来,链接到可执⾏⽂件中去,使可执⾏⽂件在运⾏时不需要依赖于动态链接库;
- 动态编译:可执⾏⽂件需要附带⼀个动态链接库,在执⾏时,需要调⽤其对应动态链接库的命令。所以其优点⼀⽅⾯是缩⼩了执⾏⽂件本身的体积,另⼀⽅⾯是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只⽤到了链接库的⼀两条命令,也需要附带⼀个相对庞⼤的链接库;⼆是如果其他计算机上没有安装对应的运⾏库,则⽤动态编译的可执⾏⽂件就不能运⾏。
7.3 动态链接和静态链接区别
静态连接库就是把 (.a)
⽂件中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需要其它的库⽂件;动态链接就是把调⽤的函数所在⽂件模块(.so)
和调⽤函数在⽂件中的位置等信息链接进⽬标程序,程序运⾏的时候再从 (.so) 中寻找相应函数代码,因此需要相应(.so)
⽂件的⽀持。
静态链接库与动态链接库都是共享代码的⽅式,如果采⽤静态链接库,则⽆论你愿不愿意,(.a)
中的指令都全部被直接包含在最终⽣成的可执行⽂件中了。但是若使⽤ (.so)
,该(.so)
不必被包含在最终 可执行⽂件 ⽂件中,可执行⽂件 ⽂件执⾏时可以“动态”地引⽤和卸载这个与 可执行⽂件 独⽴的 (.so)
⽂件。
静态链接库和动态链接库的另外⼀个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,⽽在动态链接库中还可以再包含其他的动态或静态链接库。
动态库就是在需要调⽤其中的函数时,根据函数映射表找到该函数然后调⼊堆栈执⾏。如果在当前⼯程中有多处对(.so)
⽂件中同⼀个函数的调⽤,那么执⾏时,这个函数只会留下⼀份拷⻉。但如果有多处对 (.so)
⽂件中同⼀个函数的调⽤,那么执⾏时该函数将在当前程序的执⾏空间⾥留下多份拷⻉,⽽且是⼀处调⽤就产⽣⼀份拷⻉。
7.4 动态联编与静态联编
在 C++ 中,联编是指⼀个计算机程序的不同部分彼此关联的过程。按照联编所进⾏的阶段不同,可以分为静态联编和动态联编;
- 静态联编是指联编⼯作在编译阶段完成的,这种联编过程是在程序运⾏之前完成的,⼜称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调⽤(如函数调⽤)与执⾏该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引⽤的类型。其优点是效率⾼,但灵活性差。
- 动态联编是指联编在程序运⾏时动态地进⾏,根据当时的情况来确定调⽤哪个同名函数,实际上是在运⾏时虚函数的实现。这种联编⼜称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中⼀般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使⽤动态联编。
- 动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引⽤来调⽤虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引⽤名.虚函数名(实参表)
实现动态联编三个条件:
- 必须把动态联编的⾏为定义为类的虚函数;
- 类之间应满⾜⼦类型关系,通常表现为⼀个类从另⼀个类公有派⽣⽽来;
- 必须先使⽤基类指针指向⼦类型的对象,然后直接或间接使⽤基类指针调⽤虚函数;
8、C++模板全特化和偏特化
模板分为类模板与函数模板,特化分为特例化(全特化)和部分特例化(偏特化)。
对模板特例化是因为对特定类型,可以利⽤某些特定知识来提⾼效率,⽽不是使⽤通⽤模板。
对函数模板:
- 模板和特例化版本应该声明在同⼀头⽂件,所有同名模板的声明应放在前⾯,接着是特例化版本。
- ⼀个模板被称为全特化的条件:1.必须有⼀个主模板类 2.模板类型被全部明确化。
模板函数:
template<typename T1, typename T2>
void fun(T1 a, T2 b)
{
cout<<"模板函数"<<endl;
}
template<>
void fun<int , char >(int a, char b)
{
cout<<"全特化"<<endl;
}
函数模板,只有全特化,偏特化的功能可以通过函数的重载完成。
对类模板:
template<typename T1, typename T2>
class Test
{
public:
Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}
private:
T1 a;
T2 b;
};
template<>
class Test<int , char>
{
public:
Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:
int a;
char b;
};
template <typename T2>
class Test<char, T2>
{
public:
Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:
char a;
T2 b;
}
对主版本模板类、全特化类、偏特化类的调⽤优先级从⾼到低进⾏排序是:全特化类>偏特化类>主版本模板类。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通