C++知识点

想要短期冲刺?

想要扎实点?

https://awesome-programming-books.github.io

C++基础知识

智能指针

为什么有?因为很多程序员申请指针后,忘记释放,导致内存泄露。
智能指针可以很大程度解决这个问题,其本质就是一个类,超出作用域后自动释放资源,避免这个问题。

auto_ptr(所有权)

每一个指针是唯一的拥有者。后续已经废弃

auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错
cout << *p1 ; // 炸了

为什么会这样?因为根据设计理念,你已经把所有权给p2了,那你的p1就没用了,应该去释放。同时,如果指向资源,那你多个智能指针指向同一个,资源就会被释放多次。

unique_ptr

为了解决上面的问题,这里不能够直接通过 = 来赋值,必须通过move来转译控制权。一定程度上保证了安全。

unique_ptr<string> p3 (new string (auto));//#4
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错

shared_ptr

unique_ptr用着不方便,有时候确实要多个指。
因此使用shared_ptr共同管理资源,那怎么实现这个“智能指针”呢?这里会引入计数器,当所有的shared_ptr都被销毁,那它就会释放管理的内存
那么为什么要make_shared?这个就相当于直接构造了,

问题:两个互相引用,不就永远无法释放了?

weak_ptr

为了解决shared_ptr可能的循环引用问题,用一个weak_ptr辅助shared_ptr
特点:作为观察者,不会给shared_ptr增加计数,但也不可以访问资源,只能看资源有没有被释放。如果要使用资源需要通过Lock方法转化为shared_ptr才可以(此时便不会有循环引用,因为转化的先被释放回去,不影响原先另一个shared_ptr)。

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
};

class B {
public:
    std::weak_ptr<A> a_ptr;   // B 持有 A 的 weak_ptr
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b_ptr = b;  // A 持有 B
    b->a_ptr = a;  // B 持有 A 的 weak_ptr(不增加引用计数)

    // 到这里,a 和 b 的引用计数都为 1
    return 0; // 程序结束时,资源正常释放
}

内存分配

  • 栈区:存放局部变量,临时变量,函数参数等。由编译器自动管理,分配速度快,适合小规模数据。
    • 特点:先进后出(就是栈的特点),函数返回,栈上临时变量都杀了。
  • 堆(Heap):存放malloc new出来的空间。程序员手动管理,适合大规模数据。
    • 分配比较慢,而且容易内存泄漏,空闲碎片等。
    • 名字就是早期习惯,和堆一点关系没有。
  • 全局/静态存储区:存储初始化和未初始化的全局/静态变量
  • 常量存储区:存储常量
  • 代码区:存储二进制代码(包括所有函数代码,只读)

指针参数传递和引用参数传递

指针传递本质仍然是值传递,即新建了一个临时变量,复制了指针的值。这对于指针当然不影响,指向地址不变,所以你仍然可以修改原先的值。
引用的本质是起别名,编译器转换过来后就没有额外开销了,因为不会注册新的变量。在作为函数参数的时候,放过来的是实参变量的地址,切绑定后不可以更改了。

语法上,指针要显示解引用,而引用就直接用就好了。

int a = 10;
int& ref = a;  // 不分配新内存,ref就是a的别名
int* p = &a;   // 分配了8字节(64位系统)存储地址

Const和Static

Static

作用是 控制变量/函数存储方式(放到全局静态区去),生命周期(全局),作用域(当前作用域)

修饰局部变量

作用是把局部变量从栈区移动到静态存储区,作用域仍然只在语句块中,主要来保留函数调用状态的。

void counter() {
    static int count = 0;  // 存储在静态区
    count++;
    cout << count << endl;
}// 调用 counter() 3次会输出 1, 2, 3

修饰全局变量或函数

作用:将全局变量/函数的作用域限制在当前文件。如果你不限制,其它文件可以extern访问内容。同样,如果多个文件都有同名全局变量,连接时候就会报错。因此相当于锁死在作用域里面,对外不可见

// File1.cpp
static int global = 10;      // 仅 File1.cpp 可见
static void func() { ... }    // 仅 File1.cpp 可调用

// File2.cpp
extern int global;  // 错误!无法访问 static 全局变量

修饰成员变量

个人理解:就是变成全区变量但是作用域必须要类里面。

特点:所有这个类生成出来的对象都共享这一个变量,在内存中只有一个副本。生命周期从程序开始到结束。级别相当于类级别的,不依靠对象实例化,因此哪怕类还没实例化也可以用(类没实例化的话不占内存)。

class MyClass {
public:
    static int count; // 声明
};
int MyClass::count = 0; // 定义并初始化

int main() {
    cout << MyClass::count; // 不依靠类的实例化来实现。
    MyClass obj1;
    MyClass obj2;
    obj1.count = 5;      // 通过对象访问
    cout << obj2.count;  // 输出 5(共享同一副本)
    cout << MyClass::count; // 直接通过类名访问
}

修饰成员函数

特点:没this指针(那不是当然,和上面情况类似),只能够访问类中的静态成员(毕竟类对象都没有分配),可以直接调用(同成员函数),不能够被virtual修饰(有意义吗?)

MyClass::printCount();

作用呢,可以定义一个好的工具类,封装在一起就很方便。

class MathUtils {
public:
    static double PI;
    static double circleArea(double radius) {
        return PI * radius * radius;
    }
};
double MathUtils::PI = 3.1415926535;

int main() {
    cout << MathUtils::circleArea(5); // 直接调用
}

Const

定义常量,保护数据不被修改。比如函数传进来一个参数,你const住保护了

int a = 10;
const int* p1 = &a;      // 数据不可变(理解成 const int 类型的指针,那自然数据不可变)
int* const p2 = &a;      // 指针不可变
const int* const p3 = &a; // 数据和指针均不可变

还可以放在函数括号前,表示把this指针变成const this指针。因此,此时只有const类型的成员变量才可以被使用(每个函数成员变量前本来加的this-> 现在都变成 const this->。因此只有const类型才能够用,不然类型不匹配)

class MyClass {
    int value;
public:
    void set(int v) { value = v; }         // 非 const 函数
    int get() const { return value; }      // const 函数
};

const MyClass obj;
obj.get();     // 正确
obj.set(10);   // 错误!const 对象不能调用非 const 函数

修饰类对象表示该对象只读

const MyClass obj;
obj.get();     // 正确
obj.set(10);   // 错误!

C和C++区别

核心理念:C面向过程,C++面向对象(封装、继承和多态)
内存管理:C(malloc,calloc)C++(new delete ,几个智能指针)
类型:C(隐式转换)C++(显式转换(但short转int这种可以不用说),还加了引用概念)

函数

特性CC++
函数重载(overload):也可以被称为静态多态不支持,因为c的名字修饰只有函数名称支持,c++编译后名字修饰会含传入参数信息
名字修饰void func(int) → _funcvoid func(int) → _Z4funci
函数名长4,叫func,传入int
虚函数有,靠这玩意实现动态多态
默认参数支持默认参数(如 void func(int a = 0)),原理是,如果位置为空就压入默认值。

那现在也不难理解,c++ 重载时候,支持参数不同而不支持返回值不同了,原来就是名字修饰没有存信息hhhh

C的struct只有数据,没继承多态。更多的是数据结构实现体。而c++是一个对象实现提
C++的struct和class区别就只剩下默认访问权限了。strcut public 而 class private。

其它

STL容器,模版之类的。

C++和Java区别

c++核心理念:提供高性能、底层控制与灵活性,同时兼容 C 语言的“零开销抽象”。
Java核心理念:简化开发、强调跨平台性和安全性,通过“一次编写,到处运行”(Write Once, Run Anywhere)实现高可移植性。

特性C++Java
内存管理手动(new / delete)自动垃圾回收。因为所有new操作都是在堆,后台程序自动挥手。
平台依赖需要针对不同的系统分别编译,因此C++最后直接把代码翻译成特定 CPU 架构的机器指令(如 x86、ARM),而不同操作系统提供的接口不同,因此需要去翻译移植。编译一次,生成了字节码文件(.class),对于这个文件任何安装了JVM(java虚拟机)的都可以运行,所以本质是就是运行时候放在虚拟机里面,可见效率会滴。
多重继承支持只能继承一个类,但支持继承多个接口。接口interface,那你后面就要implements,标上@override
系统类型与泛型C++模板编译时候展开,运行时候就没有开销了泛型:类型擦出的“语法糖”。Java泛型在编译后会擦除类型参数,例如,List<String> 和 List<Integer> 在运行时都表现为 List(原始类型)。替换为object对象,牺牲了灵活性和运行时信息。
指针无,对象即引用。赋值操作就是引用。函数参数传递会改变原来对象。
面向对象部分完全,除了基本数据类型之外,其余的都作为类对象
异常处理 可抛出任意类型(如 int、字符串),不强制捕获异常。必须捕获或声明抛出的异常(如 IOException)。都继承自同一个类

C++和C#区别

希腊奶

C++怎么定义常量?

局部常量(除了字符串),放在栈区(有的可以直接变成立即数)
全局常量:一般直接转化成立即数了。
字符串要放在常量区,因为要分配空间用

重载,重写,隐藏

重载(overload):同一个函数名,不同作用
重写(override):虚函数继承用。
隐藏(hide):与父类重名函数,父类的就隐藏了。

构造函数

默认构造函数:来就有,
一般构造函数:重载构造函数,同一个名字,但可以传入不同参数
拷贝构造函数:把一个对象作为参数copy进来,注意深拷贝问题

注意区分以下问题

A a1 , a2;
a1 = a2; // 赋值运算
A a3 = a1;// 拷贝构造函数(沟槽的语法乱成依托)

四种强制转换

static_cast

最常用安全的转换,上行转换安全(子类转成基类,基类里面一定有),下行不安全( 不会检查),因为可能有没有实现的部分。

dynamic_cast

专门用在虚函数父子类之间转化(必须有虚函数),会检查实际的指向。如果不是目标类型就返回空指针。因为要检查,所以实际性能略差。

const_cast

去除const属性

reinterpret_cast

从底层重新解释,告诉内存这块二进制你就这么解释。高危,别用

野指针和悬空指针

野指针:没有被初始化,如 int *p
悬空指针:指向已经被释放了的指针。

函数指针

是什么?一个指向函数的指针。编译时候,每个函数都有一个入口,那我直接指向这里,也可以调用函数。

首先是怎么理解 char* (*pf)(char* p) 认为是一个pf指针,指向了接收(char*)返回char*的函数。那么此时,对于这一类函数,你都可以去☞。

// 假设有一个函数,功能是处理字符串
char* fun(char* p) { ... } // 函数名叫fun

// 声明一个(函数指针pf),它能指向所有“接收char*,返回char*”的函数
char* (*pf)(char* p); 

// 把pf对准(指向)fun函数
pf = fun; 

// 按下遥控器的按钮——通过pf调用fun
pf(p); 

那用途就有两种,调用不同函数/回调函数

void 加密A(char* data) { ... }
void 加密B(char* data) { ... }

int main() {
    void (*加密方法)(char*); // 声明一个“加密遥控器”
    
    加密方法 = 加密A; // 今天用A加密
    加密方法(数据);   // 实际调用加密A
    
    加密方法 = 加密B; // 明天换B加密
    加密方法(数据);   // 实际调用加密B
}

对于回调函数,返回我需要的特定的处理方式

// 你告诉系统:“收到消息时,调用我的处理函数”
void 收到消息时回调(void (*callback)(char*)) {
    char* 消息 = 监听网络();
    callback(消息); // 收到消息后,调用你提供的函数
}

// 你的处理函数
void 我的处理(char* 消息) { ... }

int main() {
    收到消息时回调(我的处理); // 注册你的函数
}

堆和栈

栈:编译器管理,一般保存局部变量和函数参数,编译器自动回收。

再调用函数时候,先入栈主函数下一条地址(即func执行完成后,要返回的地址),再把函数参数从右往左插入,这里是为了便于解析可变参数类型。比如下面,我最后一个压入count,那我出来的时候,就第一个出来,就知道参数啥。

#include <stdio.h>
#include <stdarg.h>

// 可变参数函数示例
void print_sum(int count, ...) {
    va_list args;
    va_start(args, count);  // 从栈顶读取 count 之后的参数
    int sum = 0;
    for (int i = 0; i < count; i++) {
        sum += va_arg(args, int);
    }
    va_end(args);
    printf("Sum: %d\n", sum);
}

int main() {
    print_sum(3, 10, 20, 30);  // 参数入栈顺序:30 → 20 → 10 → 3
    return 0;
}

堆:程序员手动管理,new delete malloc free之类的去管理。内部空间是一块一块的,需要去管理。(ics手动做过)

new/delete/malloc/free

都是在堆上分配回收。new/delete是操作符(可以改构造函数/析构函数用),malloc/free是库函数(标准库提供的,改不了)

new:先malloc,再分配类型对象
delete:先析构,再free内存

为什么要有new和delete?因为malloc和free对于动态对象很难实现。而new和delete会自动调用构造和析构函数,保证初始化和析构正确性。

Volatile和extern

volatile:告诉编译器,这个值可能会被程序外其他因素意外修改,因此你不要优化或者假设稳定性。比如说要连续多次读取,你就老老实实一次次从地址读,不要从寄存器去整了

1.易变性,每一次读取都可能不同

volatile int sensor_value;
// 假设 sensor_value 是硬件传感器的实时值
int a = sensor_value; // 从内存读取
int b = sensor_value; // 再次从内存读取,而不是复用寄存器中的 a 的值

2.不可优化性

volatile bool flag = false;
while (!flag) { /* 等待外部事件修改 flag */ }
// 编译器不会将 while 循环优化为 if (!flag) { while(1); }

3.顺序性

volatile int a = 1;
volatile int b = 2;
a = 3; // 操作 1
b = 4; // 操作 2
// 编译器保证操作 1 在操作 2 之前执行,不会被重排。

extern:告诉变量或者函数在别的地方定义,你去别处找。这样节省了一点预处理时间。同时在c++中,如果要按照c语言命名和调用规则编译,可以加入

// C 库函数声明
#ifdef __cplusplus
extern "C" {
#endif
    void c_library_function();
#ifdef __cplusplus
}
#endif

define和const

#define 是预处理指令,再预编译阶段,就直接简单的文本替换
const 是关键字,用来定义变量是常量。

类的大小

class A{};sizeof(A) = 1;//空类为1
class A{virtual func(){};} 4//虚函数,会有一个指向虚函数表的指针,因此多少个虚函数都多加4
class A{static int a; int b;} sizeof(A) = 4; //static不算
class A{func(){};}; sizeof(A) = 1; //函数在代码区,不占空间

封装,继承,多态

封装:类。把客观事物变成抽象的类,调用时候只调用方法。
继承:让一个类获得另一个类能力,便于扩展
多态:同一个名字,多种不同的实现。有静态多态(重载overload)和动态多态(override)

虚函数原理

虚函数表:当一个类中有虚函数,就会为这类生成虚函数表

先举个例子

class Animal {
public:
    virtual void speak() {  // 基类虚函数
        cout << "Animal sound" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {  // 派生类重写
        cout << "Woof!" << endl;
    }
};

int main(){
    Animal* animal = new Dog();
    animal->speak();  // 输出 "Woof!"
}

原理是什么呢?当一个类有虚函数,就会生成一张虚函数表,指向实际类型的虚函数。派生类那就一定也有虚函数了,此时自己也就有了虚函数表。

当生成派生类对象时候,编译器检查出来有虚函数,就会为这个派生类生成虚函数指针,指向虚函数表。如果自己实现了,那么虚函数表指的就是自己实现的函数,否则就是继承下来的虚函数。

如果此时用一个基类的指针,指向一个派生类(Animal* animal = new Dog();)那么调用方法时候,虚函数指针就会指向派生类的,因此调用派生类函数。而其它非虚函数,还是animal自己的function。(如上图所示)

但是注意,如果没有virtual,那么就是你定义的类(animal)

class Base {
public:
    void func() { cout << "Base"; }
    int base_data = 10;
};

class Derived : public Base {
public:
    void func() { cout << "Derived"; } // 隐藏基类函数
    int derived_data = 20;
};

// 当编译器看到以下代码:
p->func();

// 编译器的工作:
1. 检查p的类型 → Base*
2. 在Base类中查找func()函数
3. 发现func()不是虚函数 → 执行静态绑定
4. 生成调用Base::func()的机器码

在调用构造函数的时候,是先父再子对象。
在调用析构函数的时候,是先子再父的。那析构函数几乎一定要是虚函数。why?

class Animal { ~Animal(){}};
class Dog : public Animal {~Dog(){}};
int main(){
    Animal* animal = new Dog();
    delete animal; //此时,如果不用虚函数,编译器根本找不到最底下那个Dog,只会调用Animal的方法,这显然会有问题!。
}

在继承时候,如果多继承,每个虚函数基类都会有一个虚函数表

那构造函数为什么不用虚函数?

  1. 虚函数机制依赖已初始化的vptr,而vptr初始化是构造函数的职责。形成一个循环悖论。
  2. 虚函数的本质是「对已有对象」进行功能调用(看不懂算了不看了)

纯虚函数

让你子类必须要去实现,同时这个基类变成抽象类(即自己不能够实例化),这在编译阶段就可以检查出来。

// 基类声明纯虚函数
class Base {
public:
    virtual void func() = 0; // 纯虚函数(无实现)
};

// 尝试实例化基类
Base obj;  // ❌ 编译错误

如果你又虚函数,定义了却不去实现。那么如果实例化时候,就仍然会报错,不过是在链接阶段(编译器不清楚你是不是写别的文件了)。其实本质和你类写了方法不实现是一个道理。

// 基类声明普通虚函数但未实现
class Base {
public:
    virtual void func();  // 普通虚函数(未实现)
};

// 实例化基类(合法)
Base obj;  // ✅ 编译通过

// 调用未实现的虚函数
obj.func();  // ❌ 链接错误

虚继承

why?因为菱形继承时候,会产生二义性

此时,如果你调用d.a,是意义不明确的。因为内存此时分布如右所示

此时,你用虚继承,就可以解决,内存布局会变化。B和C虚继承了

原理是,当使用虚继承时候,会用虚基表指针指向虚基表。最后,虚基类子对象是在派生类决定的,这时候就会发现继承自同一个A,那么就不用多写几份了。

静态绑定,动态绑定

虚函数动态绑定:调用func的时候,会调用到你子类的func(通过虚函数表,但虚函数表只有指针)
默认参数的静态绑定:默认参数绑定是根据静态类型决定的(因为只存了指针,没有存变量数据)

#include <iostream>
using namespace std;

class Base {
public:
    virtual void func(int x = 10) {  // 基类虚函数,默认参数 10
        cout << "Base::func " << x << endl;
    }
};

class Derived : public Base {
public:
    void func(int x = 20) override {  // 派生类重写虚函数,默认参数改为 20
        cout << "Derived::func " << x << endl;
    }
};

int main() {
    Base* obj = new Derived();
    obj->func();  // 实际调用的是谁的默认参数?
    delete obj;
    return 0;
}
Derived::func 10  // 期望是 20,但实际是基类的默认参数 10!

所以,千万不要去重定义虚函数的默认参数!到最后,还是调用的基函数的,可能与你所想不一致。

深拷贝,浅拷贝

默认浅拷贝,对于指针只是复制值,导致内容没复制。这时候delete可能导致多次删除。此时就必须自己重写拷贝函数,保证安全。

什么时候会调用拷贝构造函数?

以值传递传入对象到函数,以值传递从函数返回对象,一个对象需要通过另一个对象初始化

为什么拷贝构造函数必需时引用传递,不能是值传递?

避免循环引用。你值传递时候,就会默认构造一次拷贝构造函数。

内存对齐

原则:每个成员的位置,都是自己大小的整数倍。同时整个结构体是最大成员的整数倍。

为什么快了?因为现代cpu都是一块一块读,比如一次读2,4,8,16之类的。如果你是从1开始,那你先读了0~3,不够,还要读4~7,然后还要小心翼翼的剔除前后多余的。这样做了很多没必要的操作,大大降低了性能。为了避免这种浪费,现代很多cpu遇到没对齐的数据就直接拒绝处理,这更是雪上加霜。

内存泄漏

申请了一片内存,但是用完之后没有释放。用valgrind扫出来

怎么避免?智能指针,stl容器,确保抛出异常时候依然正常释放

二叉树

二叉树:每个节点最多俩子节点
二叉搜索树:左节点<根节点<右节点
二叉平衡树:左右子树高度差不要过高。

红黑树

1.根节点黑 2.叶子节点黑(外部) 3.红节点不连红节点 4.从根节点,到任意叶子结点 5.插入时候默认是红的

插入原则:看叔叔脸色(具体忘了)

define,const,typedef,inline

#define:在代码编译之前的文本替换
typedef:在编译器解析没问题,把它视为真正的类型。
inline:给编译器一个建议,把这段函数嵌入过去

typedef int* INT_PTR;
INT_PTR a, b;  // a 和 b 都是 int* 类型(符合预期)
using FuncPtr = int(*)(int, int);  // 等价于 typedef int(*FuncPtr)(int, int);

预处理,编译,汇编,链接(ciso)

把一段高级语言编程二进制代码需要4个阶段:预处理,编译,汇编,链接

阶段输入输出核心任务特点:
预处理.c.i处理全部#任务
1.展开头文件#include<stdio.h>,直接插入本文件
2.宏替换 ,#define PI 3.14之类直接文本替换
3.条件编译#ifdef和#ifndef等判断是否保留
纯文本替换
编译阶段.i.s把高级语言翻译成汇编。
1.检查语法,语义是否正确
2.对代码简单优化
3.生成汇编代码(不同CPU架构不同)
翻译成汇编
汇编阶段.s.o把汇编代码变成机器指令(二进制)
1.把每一条汇编语句变成一条机器码
2.生成机器指令,数据段,符号表(不知道空着)
汇编代码 call puts 会被翻译为二进制指令
E8 00 00 00 00(假设 puts 的地址暂时未确定)。
生成二进制,但地址未最终确定
链接阶段.o .a
[库文件]
.out/.exe合并多个目标文件,解析符号地址
1.找到所有未定义的符号(如 printf)的具体地址。
链接可以是静态链接(库代码直接嵌入可执行文件)
或动态链接(运行时加载库)。

如果头文件的内容没有在.cpp中实现,会怎样?报错,但是在链接阶段,因为链接阶段找不到具体的实现。

动态编译和静态编译

  • 静态编译:把需要的库文件直接打包进最终的可执行文件
    • 好处:简单。
      坏处:文件体积大(哪怕只需一个函数却要整个库),更新要重新编译整个文件,多程序浪费
  • 动态编译:只需要记录引用的信息,运行的时候动态加载
    • 好处:文件体积小,多程序共享库,库更新不用重新编译主程序
    • 坏处:需要正确版本的运行库去解释,环境部署依赖库(python)

静态链接和动态链接

  • 静态链接
    • 链接时间:编译时
    • 包含内容:把lib中代码完整的复制到可执行文件
    • 更新:修改整个程序
  • 动态链接
    • 链接时间:运行时
    • 包含内容:记录文件模块(DDL)的函数位置信息
    • 更新:更换DDL就更换了功能

动态联编与静态联编

  • 静态联编(早绑定)
    • 编译时候绑定,根据指针/引用的类型声明
    • 特点:效率高(运行时无开销),但不太灵活
class Base { void func(); };
class Derived : public Base { void func(); };

Base* obj = new Derived();
obj->func(); // 调用Base::func(静态绑定)
  • 动态联编(晚绑定)
  • 运行时绑定(通过虚函数表去看)
  • 灵活,但有开销
class Base { virtual void func(); };
class Derived : public Base { void func() override; };

Base* obj = new Derived();
obj->func(); // 调用Derived::func(动态绑定)

友元

是一种打破类封装的机制,允许其他类或函数访问当前类的私有(private)或者被保护(protected)成员

class Box {
private:
    int width;
public:
    Box(int w) : width(w) {}
    // 声明友元函数
    friend void printWidth(Box box);
};

// 友元函数可以访问 Box 的私有成员
void printWidth(Box box) {
    cout << "Width: " << box.width << endl;
}

int main() {
    Box b(10);
    printWidth(b); // 输出: Width: 10
}

类和数据抽象

类与类关系:
has-A:B类用了A类作为变量,声明周期相同
use-A:B类用了A类作为返回值或者函数变量。声明周期临时
is-A:本质就是父子类的层次关系

继承特点:
子类拥有父类全部属性方法,子类可以拥有父类没有的方法,子类对象可以当做父类对象使用。

组合:内嵌其它类对象作为自己成员

什么时候需要成员初始化列表?

什么是初始化列表?

class MyClass {
public:
    // 初始化列表语法
    MyClass(int a, int b) : m_a(a), m_b(b) { 
        // 构造函数体(此时成员已初始化完毕)
    }

private:
    int m_a;
    int m_b;
};

这是在对象创建时,直接初始化成员变量。

为什么需要?

总结:对于必须初始化的变量(const,引用),没有默认构造参数的,在对象创建之初就给他分配好。

初始化const

class Circle {
public:
    Circle(double r) : PI(3.14159), radius(r) {} // ✅必须用初始化列表
private:
    const double PI;  // const成员
    double radius;
};

初始化引用

引用必须要初始化时候绑定,也不能更改

class Student {
public:
    Student(int& id) : ref_id(id) {} // ✅必须用初始化列表
private:
    int& ref_id;  // 引用成员
};

调用基类的有参构造函数

如果基类没有默认构造函数,那就必须要显示调用(其它时候编译器隐示的调用了)

class Base {
public:
    Base(int x) { /*...*/ }
};

class Derived : public Base {
public:
    Derived(int a) : Base(a) { /*...*/ } // ✅显式调用基类构造函数
};

初始化成员对象

成员对象类没有默认构造函数

执行顺序

成员变量的初始化顺序由类的声明顺序决定(与初始化列表无关)。
基类构造函数先于派生类构造成员初始化

class Example {
public:
    Example(int x) : b(x), a(b) {} // ❌危险!a先初始化,但此时b未初始化
private:
    int a;  // 先声明
    int b;  // 后声明
};

与构造函数体赋值对比

方式初始化列表构造函数体内赋值
本质直接初始化(调用构造函数)先默认构造,再赋值
适用对象所有对象非const,引用
效率高效率(避免二次操作)低效率(构造+赋值是两步)

STL容器

底层数据结构特性迭代器数据结构
vector动态数组1.连续存储,随机访问o1
2.动态扩展,按指数分配空间
普通指针可以完成三个指针:
start(当前使用空间头)
finish(当前使用空间尾)
end_of_storage(可用空间尾)
list双向链表1.非连续存储,双向链表存储
2.根据实际节点分配内存
BidirectionalIterator
支持++和–,但无法随机跳跃
一个指针指向头结点
deque双端队列1.分段连续存储,逻辑上连续,
物理上分段,支持双向动态扩容。
即,可以O(1)随机访问
RandomAccessIterator
包含四个指针
cur,first,last,node

start和finish迭代器分别指向守卫缓冲区
stack1.底层容器默认deque
2.先进后出
依赖底层容器
queue队列1.底层容器默认deque
2.先进先出
依赖底层容器
priority_queue优先队列1.容器默认vector
2.本质是堆
无直接访问

Vector的push_back和emplace_back

push_back():需要先就地构造对象,然后拷贝,然后再释放原有对象,效率比较低
emplace_back():直接在容器内构造对象,就用这种+强制类型转换

map和set区别

联系:底层都是红黑树

特性mapset
存储内容键值对(key-value单个键(key
修改键(key)❌ 不可修改(影响排序)❌ 不可修改(直接是元素本身)
修改值(value)✅ 可修改(不影响排序)无值(元素即键)
下标操作✅ 支持(按 key 查找或插入)❌ 不支持
迭代器行为键只读,值可修改迭代器完全只读(元素不可修改)

对于map中,如果访问不存在的,会返回一个默认值。

迭代器

是STL容器的通用指针

为什么需要?因为如果每种容器都用自己的遍历方式,写一个通用的“查找算法”需要针对不同容器写多份代码。迭代器的核心价值就是统一所有容器的遍历接口

int arr[5] = {1,2,3,4,5};       // 数组
list<int> myList = {1,2,3,4,5}; // 链表

有些容器支持随机访问,比如vector,deque,
有些可以前后访问,比如list,set,map。用++,–往前后

其本质不是指针,但表现得很像指针。你可以用指针的方式去访问比如*,->等。但本质是类对象,只是重载了操作符。

迭代器失效

迭代器失效:指向元素的指针可能因为内部结构变化,指向无效的数据。

vector<int> v = {1, 2, 3};
auto it = v.begin() + 1; // 指向 2
v.push_back(4);          // 可能导致扩容
// 此时 it 可能指向无效内存,访问 *it 会导致未定义行为

STL删除元素

vector和deque,删除后后面所有元素迭代器都失效了,但是会往前一个,earse返回下一个有效迭代器
比如s.begin()+2,后面肯定用不了了
map set,使用earse后,这个迭代器就失效了,不影响其它的。需要你提前记录下一个有效的迭代器。

resize和reserve区别

reserve:预留容量,避免多次动态扩容。不会删元素
resize:直接修改元素数量,使当前vector大小为size。若n>size,则初始化0,n<size,则多余的部分被析构。

模板类的实现

在编译器处理模板时候,会分为两步,一步检查模板本身有没有问题,第二是根据类型生成实际代码

第一次编译(模板代码检查):此时编译器只是“记住”这个模板的存在,但不会生成任何实际函数。

// 模板声明和定义(在头文件中)
template <typename T>
void mySwap(T& a, T& b) {   // 第一次编译:检查语法
    T tmp = a;
    a = b;
    b = tmp;
}

第二次编译(实例化代码生成):根据实际类型如int等,生成模板函数

// 实例化生成的具体代码(编译器自动生成)
void mySwap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}

++i和i++

++i不会有临时变量。i++会产生临时变量

大小端怎么检测

int个数,强制转成byte*,看看第一个是啥

int(*p)[10] int *p[10] int *f(int i) int(*f)(int i) 的区别

*结合优先度很低,如果结合就表示指向…..的指针。第一个为指向数组的指针,第二个是含有10个指针的数组,第三个是返回值为int *的指针,第四个是函数指针,指向接收int返回int的函数

进程线程协程

进程:操作系统资源分配基本单位,拥有独立的内存空间。操作系统会以进程为单位分配系统资源(内存,时间片等)
线程:进程之间共享同一进程资源(内存,文件句柄等),创建开销比较少。每个线程有自己的调用栈,适合CPU密集型(因为在cpu上可能真的多核在跑)
协程:轻量级线程,在单线程内异步切换。所有协程共享栈,因此切换时会保存和恢复,适合需要io密集型(本质上就还在一个线程里,只是切换得快,这样懒得去等了)

函数调用开销

  • 传参开销,值传递复制开销,引用传递开销
  • 函数的调用栈:保存调用者上下文,分配栈帧,回复上下文
  • 返回值开销:新建对象
  • 缓存失效:丧失了局部性,新上来页把原先的挤掉了

i++是否线程安全?

不安全,因为不是原子操作。涉及到取到原数,加,再赋值回去。

函数都放在哪里

代码段中,不占用类的大小。虚函数表放在程序只读数据段+-

左值与右值和移动语义

左值:有地址,可以取地址,出现在等号左侧。
右值:通常没有地址,存储在寄存器或者临时变量。不能&,字面量表达式结果都是右值

右值引用时候,比如说 int && a = 10; 10一般没有地址,那怎么办呢,那就把10放在栈上(分配临时空间),然后a变量指向这个数(临时变量)。

纯右值:一些数据啥的,比如说10,1.1,bool之类的

move:把左值变为右值。底层到汇编来说,和你直接int &b = a;是一样的,都是起了一个别名。那有什么作用呢?目的就是为了标记这个数现在是右值了,编译器要做右值有关的事情了,比如触发移动构造,避免深拷贝。

int a = 10;
const int &&c = move(a);

什么意思呢?比如说一个string,你赋值a = b,那默认就是深拷贝,每个都有一个数据,那就造成了资源的浪费。对于一个实现的很好的类也是如此,你会深拷贝数据。有时候我们不需要,就只是想指向这唯一一个数据避免浪费,就有了move。元数据内容也就为空了。

string str1 = "hello";
string str2 = move(str1);

cout << "str1:" << str1 << endl; // 为空
cout << "str2:" << str2 << endl; // hello,直接把数据偷走,即交出所有权。

至于底层实现,那就更直接了。就是static_cast强制转换为右值类型。

模板常见用途

对于一些数据结构,可以通用的实现。

编译时长因素

  • 最直接的,代码量。源文件数量越多,行数越多,编译时间越长。
  • 头文件或者重复包含,或者用了很多inline,也会导致这个问题。
  • 模板类,为每个新实例(int double之类的)都会生成新的符号(函数,类名),编译器需要维护实例化信息。
  • 编译器,不同的优化策略,不同的优化级别(-O2 -O3)

C++11新特性

空指针 nullptr

nullptr专门用来表示空指针,类型是 std::nullptr_t,可以被隐式转换为任何指针类型(如 int*char* 等),但不能够转换为整数
NULL是C语言留下来的宏,本质就是0或(void*)0,要看编译器

这样可以避免歧义,保证类型安全。比如下面,你具体就不知道调用到哪里去了,很危险。

void func(int*);
void func(double);

func(NULL);  // 可能调用 func(double)(如果 NULL 是 0)

Lambda

本质是匿名函数。

[捕获列表] (参数列表) -> 返回类型 { 
    函数体 
};
  • 捕获列表:定义lambda对外部变量的可见性
  • 参数列表:就是函数参数,一样的东西
  • 返回类型:可省略,即 ->返回类型这一块一起省略,编译器可以推断出来
  • 函数体:具体实现的逻辑

常见几个用途:1.STL比较函数 2.回调函数 3.闭包

// STL比较器
vector<int> nums = {3, 1, 4, 1, 5};
sort(nums.begin(), nums.end(), [](const int &a,const int &b) {
    return a > b; // 降序排序
});

//回调函数
void processData(const vector<int>& data, function<void(int)> callback) {
    for (auto num : data) {
        callback(num);
    }
}
processData(nums, [](int num) {
    cout << num * 2 << " ";
});

//闭包(保存状态)
auto makeCounter = [](int start) {
    return [start]() mutable {
        return start++;
    };
};
auto counter = makeCounter(5);
cout << counter(); // 5
cout << counter(); // 6

右值引用

那先得搞明白什么是左,什么是右

右值:临时的不可取地址的值,比如说3,“114514”这种。
左值:可以取地址,有名字,可以持久化状态的对象。

int x = 5;       // x 是左值
int* p = &x;     // 可以取地址
int y = x + 3;   // x+3 是右值(临时结果)
string s = "hello"; // "hello" 是右值(字面量)

左值引用你就直接&完事了,所以一般只能引用左值,对于常量

int &a = 7;// 错误
const int &a = 7;// 对

那为什么需要右值引用呢?对于以下案例,上面那个X x3 = make_x(),你需要有3次构造。分别是局部变量x1,作为返回值的临时变量,最后赋值过来给x3构造。

class X{ ////;} //省略具体
X make_x(){
  X x1;
  return x1;
}
int main(){
  // X x3 = make_x() 
  X && x2 = make_x();
  x2.func();
}

而使用右值,你可以减少一次,最后一次赋值那一步不用赋值,直接用它返回的那个。

编译时常量

const是运行时常量,constexpr是编译时常量,强制编译期求值。

constexpr int N = 5;  // N 是编译期常量
int arr[N];           // 合法:编译器知道 N 的值

constexpr int computeSize() { return 5; }  // 编译期函数
constexpr int M = computeSize();           // 合法:M 是编译期常量
int arr[M];                                // 合法

因此const一般效率会低些,因为运行时候才带入(但如果简单常量编译器可能在编译时候优化了),同时也支持函数,也检查了类型(毕竟编译器就可以发现问题,保证了类型安全)

类型推导

auto:忽略引用,和顶层的const和&

auto x = 5;        // int
auto y = 3.14;     // double
auto& ref = x;     // int&
const auto c = x;  // const int
const int d = 1;
auto f = d;        // int,忽视掉了const

const int* p = &x;
auto p2 = p;      // const int*(保留底层 const)

推导表达式的精确类型,包括引用和顶层 const。当然还是比较聪明的,对于临时产生的,比如说0,x+0这种纯右值,就不会给你变成int && ,而是int。而你把右值引用过来,那就是正常的int &&。

int x = 5;
const int& rx = x;
decltype(x) a = x; // int
decltype(x + 0) a1 =x //int
decltype(rx) b = x; // const int&
decltype((x)) c = x; // int&(括号导致推导为引用,(x)是左值表达式)

class A {
    xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值
decltype(c)                   // A &&

哈希表

容器类型是否存储键值对是否允许重复键底层实现查询复杂度(平均)
std::unordered_set否(仅键)哈希表O(1)
std::unordered_multiset否(仅键)哈希表O(1)
std::unordered_map是(键值对)哈希表O(1)
std::unordered_multimap是(键值对)哈希表O(1)

mult允许多个重复键,因此,你可以查出全部等于这个值的

std::unordered_multimap<std::string, int> multi_scores;
multi_scores.insert({"Alice", 90});
multi_scores.insert({"Alice", 95});  // 允许重复键

// 查找所有 "Alice" 的分数
auto range = multi_scores.equal_range("Alice");
for (auto it = range.first; it != range.second; ++it) {
    std::cout << it->second << std::endl;  // 输出 90 和 95
}

底层就是桶,映射到一个桶后面链表或者红黑树。

  • 自旋锁:while循环自旋等待。
    • 好处反应快,简单,不需要上下文切换。坏处吃cpu资源
  • 互斥锁:如果锁被占用,就会把这个资源挂起
    • 好处是节省cpu资源,但是为了记录这个挂起状态也会消耗cpu资源
  • 读写锁:读都可以读,但写只有一个可以写
  • 信号量:多个可以进入资源,灵活多变

在c++里面

锁类型核心特性性能开销适用场景
std::mutex最基础的互斥锁,不可重入,不支持超时。简单的独占访问(需手动管理生命周期)。
std::lock_guard简单的 RAII 锁,构造时加锁,析构时解锁。极低作用域内的简单互斥访问(无需手动解锁)。
std::unique_lock支持延迟加锁、手动解锁、条件变量、所有权转移。需要灵活控制锁的场景(如条件变量)。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇