之前写的总结:
目录
目录
二十一、数组和指针
数组:
- 静态分配:在栈上分配连续内存,无法根据需求动态调整大小。
- 固定大小:数组的大小在编译时确定。
- 编译器优化:由于分配在栈上,访问速度快,编译器可以更好地优化。
- 局限性:如果大小过大或无法预测,可能导致栈溢出。
- 强类型检查:数组绑定特定类型,访问时编译器会检查类型是否匹配。
- 隐式转换:不支持隐式类型转换,使用更安全。
指针:
- 指针本身:占用固定大小的内存(根据系统的地址长度,为4字节或8字节)。
- 动态分配:通过new或malloc分配堆内存,内存大小可以在运行时指定。
- 灵活性:可以指向任何内存位置(栈、堆、静态内存等),也可以强制转换为其他类型,容易引发类型安全问题。
- 内存管理:需要手动释放分配的堆内存(避免内存泄漏)。
特性 | 数组 | 指针 |
内存分配 | 静态分配,速度快 | 动态分配,灵活但需管理 |
大小灵活性 | 固定大小,声明时确定 | 动态分配,运行时可调整 |
类型安全性 | 强类型检查,安全性高 | 类型转换灵活,可能不安全 |
操作灵活性 | 无法直接算术运算,偏严格 | 支持指针运算,访问灵活 |
内存管理 | 自动释放,无需手动管理 | 需手动释放内存,否则可能泄露 |
选择建议:
如果大小固定且易于管理,优先使用数组。
如果需要动态大小或跨函数共享,使用指针。
二十二、类型转换
1. static_cast
用于在有明确定义的类型之间进行转换。
适用于
- 基本数据类型的转换(如int转换成float)
- 类层次结构中,从派生类到基类的安全转换
- 忽略const和volatile的限定符。
- 转换非多态类型的指针。
#include<iostream>
usingn namespace std;
class Base {};
class Derived : public Base {};
int main(){
// 基本类型转换
int a = 10;
float b = static_cast<float>(a);
cout << "b: " << b << endl;
// 类指针转换
Derived d;
Base* basePtr = static_cast<Base*>(&d); // 派生类 -> 基类
cout << "Pointer conversion successful!" << endl;
return 0;
}
注意事项:
- 不能用于从基类指针转换成派生类指针(非安全转换)。
- 不会进行运行时类型检查。
2.dynamic_cast
专用于多态类型的指针或引用转换
适用于
- 从基类指针转换为派生类指针。
- 检查运行时类型安全性。
条件
- 必须在类中包含至少一个虚函数
- 转换失败时,指针返回nullptr,引用抛出bad_cast异常。
#include <iostream>
using namespace std;
class Base {
public:
virtual void func() {} // 必须有虚函数
};
class Derived : public Base {};
int main() {
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr != nullptr) {
cout << "Conversion successful!" << endl;
} else {
cout << "Conversion failed!" << endl;
}
delete basePtr;
return 0;
}
注意事项
- 转换代价较高,避免频繁使用
- 如果类型层次不明确或对象不是派生类的实例,转换会失败
3.const_cast
用于移除或添加const、volatile属性
适用于
- 常用于需要对const对象进行非常规修改的场景。
#include <iostream>
using namespace std;
void modify(const int* ptr) {
int* modifiablePtr = const_cast<int*>(ptr); // 移除 const
*modifiablePtr = 20; // 修改值
}
int main() {
const int a = 10;
cout << "Before: " << a << endl;
modify(&a);
cout << "After: " << a << endl; // 输出可能是 20,但未定义行为
return 0;
}
注意事项
- 修改const对象的值可能导致未定义行为,特别是当该对象被存储在只读内存中。
4.reinterpret_cast
用于在无关类型之间的强制转换
适用于
- 指针类型之间的转换
- 将整数转换为指针,或指针转换为整数
- 不提供类型安全性检查,仅改变数据的解释方式
#include <iostream>
using namespace std;
int main() {
long p = 0x12345678;
int* ptr = reinterpret_cast<int*>(p); // 强制将 long 转为 int*
cout << "Pointer: " << ptr << endl;
// 任意类型转换
int x = 10;
float* fptr = reinterpret_cast<float*>(&x);
cout << "Reinterpreted float: " << *fptr << endl; // 未定义行为
return 0;
}
注意事项
- 这种转换不检查安全性,可能导致未定义行为
- 应尽量避免使用,除非明确知道转换的意义。
四种类型转换总结表格
转换类型 | 功能 | 使用场景 | 运行时检查 | 安全性 |
static_cast | 用于相关类型之间的显式转换 | 基本类型转换、派生类到基类指针的转换 | 无 | 较高 |
dynamic_cast | 多态类型间的安全转换 | 基类指针到派生类指针的转换 | 有 | 高 |
const_cast | 添加或移除const和volatile | 需要修改const对象(非常规操作) | 无 | 较低,需谨慎使用 |
reinterpret_cast | 在几乎无关类型之间进行的低级强制转换 | 指针类型转换、指针与整数间的转换 | 无 | 最低、慎用! |
二十三、虚函数
允许在基类中通过virtual声明一个函数,然后在派生类中对它进行重定义。
实现虚函数关键在于虚函数表(vtable)和虚函数表指针(vptr)
1.工作机制
每个含有虚函数的类都有一张虚函数表,表中存有该类的虚函数的地址。每个对象都有一个虚函数表指针来指向这个类的虚函数表。当调用虚函数时,程序会通过对象的虚函数表指针找到相应的虚函数地址,然后进行函数调用。
注:虚函数的调用比普通函数多了一个vtable查找过程,运行时有额外开销。
2.为什么析构函数要声明为虚函数
如果基类的析构函数不是虚函数,在销毁派生类对象时,只有基类的析构函数会被调用,而派生类的析构函数不会被调用,可能导致资源泄露。
#include <iostream>
using namespace std;
class Base {
public:
~Base() { cout << "Base destructor" << endl; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived destructor" << endl; }
};
int main() {
Base* ptr = new Derived();
delete ptr; // 错误调用,仅调用 Base 的析构函数
return 0;
}
解决方法
将基类析构函数声明为虚函数
class Base {
public:
virtual ~Base() { cout << "Base destructor" << endl; }
};
3.为什么构造函数不能是虚函数
- 构造函数用于初始化对象,而在对象构造阶段时虚函数表尚未完全建立
- 如果构造函数是虚函数,派生类无法正确调用其虚函数,导致逻辑混乱
4.还有哪些函数不能是虚函数
- 内联函数------内联函数表示在编译阶段进行函数体的替换,而虚函数在运行期间进行类型确定
- 静态函数------静态函数不属于类的某个对象,没有this指针,无法访问虚函数表
- 友元函数------友元函数是类外定义的,不属于类成员,不能出现在虚函数表中
- 普通函数------普通函数与类没有关系,不能加入类的虚函数表中
二十四、多态、纯虚函数和抽象类
多态
通常依赖于虚函数,在基类中声明虚函数,然后在派生类中进行重写。
在继承中构成多态的条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
纯虚函数
在虚函数后面写上=0,相当于一个接口名,不提供任何实现。
作用
- 用于定义接口,派生类必须重写纯虚函数
- 让基类充当一个抽象类,以确保派生类实现特定的功能
抽象类
含有纯虚函数的类称为抽象类
特性
- 不能实例化
- 可以包含普通成员函数
- 可以定义构造函数和析构函数
#include <iostream>
using namespace std;
class AbstractBase {
public:
virtual void display() = 0; // 纯虚函数
virtual ~AbstractBase() { cout << "AbstractBase destructor" << endl; }
};
class Derived : public AbstractBase {
public:
void display() override { cout << "Derived display" << endl; } // 实现纯虚函数
};
int main() {
// AbstractBase obj; // 错误:抽象类不能实例化
AbstractBase* ptr = new Derived(); // 可以通过基类指针操作派生类对象
ptr->display(); // 调用 Derived 的 display
delete ptr;
return 0;
}
二十五、虚继承
1.实现原理
- 虚继承机制------编译器在使用虚继承时,会在派生类中添加一个指针,指向唯一的基类子对象。(但会增加运行时的间接访问开销,但消除了重复继承问题)
- 对象模型------(1)在普通继承中,每个派生类会独立包含基类的成员(2)在虚继承中,基类的成员存储在共享的基类子对象中,所有虚继承的派生类都指向这个子对象。
2.解决的问题
菱形继承问题
假设有四个类:A、B、C和D,其中B和C都继承自A,而D又同时继承自B和C。这样就会形成一个“菱形”结构,D类中会包含两个独立的A类子对象,导致冗余
3.特性
- 避免重复继承问题------基类子对象在最终的派生类中只存在一份
- 解决二义性------访问基类成员时不需要指定路径
- 灵活性------虚继承适合用于复杂的继承体系,确保基类子对象不重复
4.注意事项
- 构造顺序问题------在虚继承中,基类的构造函数会有最终的派生类负责调用,中间派生类的构造函数不能直接初始化虚基类
- 运行时开销------虚继承增加了对象的内存布局复杂性,以及成员访问的运行时开销
class A {
public:
A(int v) { cout << "A constructed with value " << v << endl; }
};
class B : virtual public A {
public:
B() : A(0) {} // 错误,B 无法直接初始化虚基类 A
};
class D : public B {
public:
D() : A(10), B() {} // 正确,D 初始化虚基类 A
};
二十六、函数重载和重写
1.函数重载
定义
函数重载是指在同一个作用域内,可以定义多个函数名相同但参数不同(个数或类型不同)的函数。注:函数重载根据参数类型和个数进行区分,返回类型不同并不算重载依据。
特性
- 同名函数:重载函数的函数名相同
- 参数不同:重载函数的参数个数或参数类型不同
- 返回类型:仅通过返回类型无法区分重载函数,返回类型不构成重载。
优点
- 增加代码可读性
- 改善程序可维护性
#include <iostream>
using namespace std;
class Printer {
public:
void print(int x) {
cout << "Printing integer: " << x << endl;
}
void print(double x) {
cout << "Printing double: " << x << endl;
}
void print(const string& str) {
cout << "Printing string: " << str << endl;
}
};
int main() {
Printer p;
p.print(10); // 调用 print(int)
p.print(3.14); // 调用 print(double)
p.print("Hello"); // 调用 print(string)
return 0;
}
2.函数重写
定义
函数重写发生在 继承关系 中,子类通过重写父类的方法来实现自己的具体实现。重写函数需要与父类的函数签名(包括参数列表和返回类型)完全一致。
特性
- 发生在继承中:重写发生在子类中,子类函数与父类函数具有相同的名称和签名。
- 完全一致的签名:子类重写的函数必须与父类的函数具有相同的函数名、参数列表和返回类型。
- 动态绑定:当父类的指针或引用指向子类对象时,调用的是子类重写后的方法,这种行为在运行时决定。
#include <iostream>
using namespace std;
class Animal {
public:
virtual void sound() {
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
void sound() override { // 重写父类的 sound 方法
cout << "Dog barks" << endl;
}
};
int main() {
Animal* animal = new Dog();
animal->sound(); // 运行时调用 Dog 类的 sound(),动态绑定
delete animal;
return 0;
}
二十七、什么时候用指针,什么时候用引用
1.指针
是一个变量,存储另一个变量的内存地址。
常见场景
- 动态内存管理------需要在运行时动态分配内存时,需要使用指针。例如,在使用
new
或malloc
分配堆内存时。 - 可以指向nullptr------指针可以为空,表示没有指向任何对象
- 对象的传递或返回------当函数需要传递对象时,并且你可能希望修改传入对象的内容或避免复制时,使用指针。可以返回多个值或指向多个对象。
- 数组和字符串的处理-------数组和字符串本质上是指向元素的指针,指针可以用来遍历数组或字符串。
- 指向基类或派生类-------当你需要通过指针访问不同类型的对象(例如基类指针指向派生类对象)时,可以使用指针。
2.引用
是对另一个对象的别名,使用引用时无需解引用操作,语法更简洁。必须在初始化时绑定到一个对象,绑定过后不能再更改。
常见场景
- 函数参数传递------希望避免对象的复制并且允许修改传入的对象时,使用引用。引用提供了比指针更简洁的语法,并且不会为空。
- 常量引用传递------希望避免复制,但又不希望修改对象时,可以使用常量引用
- 避免空值------引用不能为空
- 返回值-------如果一个函数需要返回一个对象的引用(通常是类的成员变量或局部静态对象),则使用引用返回值,而非指针
- 链式调用
std::ostream& operator<<(std::ostream& os, const MyClass& obj) { os << obj.toString(); return os; }
二十八、迭代器:++it和it++哪个好
1. 前置自增操作符(++it)
首先将对象自增,然后返回对当前对象的引用。
int& operator++(){
*this+=1; //首先进行自增操作
return *this; //返回当前对象的引用
}
行为
自增操作会直接作用于对象本身,然后返回对象的引用(*this)。
优点
- 前置自增操作不会产生临时对象,因此效率较高
- 直接返回引用,可以用于链式调用,例如++(++it)
2. 后置自增操作符(it++)
首先创建一个当前对象的临时副本,然后执行自增操作,最后返回副本
int operator++(){
int temp=*this; //保存当前对象的副本
++*this; //调用前置自增
return temp; //返回副本
}
行为
自增操作会直接将当前对象的值存储在一个临时变量中,然后调用前置自增原对象,最后返回那个临时对象的副本。后置自增返回对象的副本,而不是引用,因此会涉及到拷贝。
缺点
- 由于返回的是副本,后置自增会产生额外的临时对象,导致性能下降,特别是在自增操作发生在大量循环或复杂对象上时。
二十九、构造函数的执行顺序
1.虚基类构造函数(如果有虚继承)
2.上层基类构造函数(按从上到下的顺序)
3.vptr初始化
4.成员变量初始化(按照类中声明的顺序,而不是初始化列表的顺序)
5.构造函数体内的代码
三十、字符串从开始打印到屏幕上的全过程
1.用户告诉操作系统执行HelloWorld程序
2.操作系统 : 找到HelloWorld程序的相关信息,检查其类型是否为可执行文件,并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块位置
3.操作系统 : 创建一个新进程,将可执行文件映射到该进程结构。
4.操作系统 : 为该程序设置CPU上下文环境,并跳到程序开始处。
5.执行HelloWorld程序的第一条指令,发生缺页异常。
6.操作系统 : 分配一页物理地址,并将代码从磁盘读入内存,然后继续执行程序。
7.程序执行puts函数,在显示器上写入。
8.操作系统找到要将字符串送往的显示设备,发送给控制该设备的进程。
9.操作系统 : 控制设备的进程告诉设备的窗口系统所要显示的字符串。窗口系统确认操作后将字符串转换成像素,并写入设备的存储映像区
10.视频硬件将像素转换成显示器可接收和一组控制数据信号
11.显示器解释信号,激发液晶屏
12.显示成功。
总结
究极折磨,后天联通笔试,再往后还有中石化笔试。什么时候才是个头啊!
加油,争取还是一天一更!!!!