C++相关知识总结

模板

函数模板是C++中的一种泛型编程机制,允许我们编写与类型无关的函数。通过使用模板参数,我们可以创建一个函数模板,该模板可以接受不同类型的参数,并在编译时生成相应的函数实例。

#include <iostream>
using namespace std;

template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    cout << max(3, 5) << endl;
    cout << max(3.5, 5.5) << endl;
    return 0;
}

尖括号内的typename可以替换为class,两者在模板参数列表中是等价的:

可以存在多个模板参数:

template<typename T1, typename T2>
void print(T1 a, T2 b) {
    cout << a << " " << b << endl;
}

本质上,模板就是编译器在编译时根据模板参数生成具体的函数或类实例。模板参数可以是类型参数(如上例中的T)或非类型参数(如整数、指针等)。这种自动生成代码的机制被称为隐式实例化。

显式实例化是指程序员手动指定模板参数来生成特定类型的函数或类实例。例如:

template class MyClass<int>; // 显式实例化MyClass<int>
显式实例化可以用于减少编译时间或控制生成的代码量,例如,在头文件中对显式实例化进行声明:
// MyClass.h
extern template class MyClass<int>; // 声明显式实例化
随后,在源文件中进行定义:
// MyClass.cpp
template class MyClass<int>; // 定义显式实例化

这样,即使有复数文件包含了MyClass<int>的使用,编译器也只会生成一个实例,从而减少编译时间和代码冗余。

特化(Specialization)是指为特定类型或特定条件提供不同的实现。C++支持两种特化:完全特化和部分特化。完全特化是指为特定类型提供一个完全不同的实现。例如:

template<>
class MyClass<int> {
public:
    void display() {
        cout << "This is a specialization for int." << endl;
    }
};
对于类模板,特化需要对构造函数和成员变量、成员函数等进行重新定义。 特化模板类可以包含原模板类不曾有的成员函数或成员变量,但不能删除原模板类中的成员函数或成员变量。

部分特化(又称偏特化)是指为满足特定条件的类型提供一个不同的实现。例如:

template<typename T>
class MyClass<T*> {
public:
    void display() {
        cout << "This is a partial specialization for pointer types." << endl;
    }
};
此外,在具有两个模板参数的类模板中,把其中一个参数固定为特定类型,而另一个参数保持为模板参数的特化也属于偏特化。例如:
template<typename T>
class MyClass<T, int> {
public:
    void display() {
        cout << "This is a partial specialization for T and int." << endl;
    }
};
对于有复数个模板参数的类模板,如果一个偏特化模板把第M个模板参数固定为类型A,另一个偏特化模板把第N个模板参数固定为类型B,而编写代码时定义了第M个模板参数类型为A,第N个模板参数类型为B的实例,此时会发生二义性错误,因为编译器无法确定应该使用哪个偏特化模板。这种情况下,需要定义一个更具体的偏特化模板来解决二义性问题。

偏特化仅适用于类模板,函数模板不支持偏特化。

当同时存在函数模板和普通函数时,具体使用哪个取决于是否需要进行自动类型转换。如果调用函数时提供的参数类型与普通函数完全匹配,编译器会优先选择普通函数。如果需要进行自动类型转换,编译器会选择函数模板。

对于类模板,模板之内的成员函数定义可以放在类定义内,也可以放在类定义外。如果成员函数定义在类定义内,则该函数会被隐式地视为内联模板成员函数;如果成员函数定义在类定义外,则需要使用template关键字来指明这是一个模板成员函数。例如:

// MyClass.h
template<typename T>
class MyClass {
public:
    void display(T value);
};

// MyClass.cpp
template<typename T>
void MyClass<T>::display(T value) {
    cout << "Value: " << value << endl;
}

非类型参数是指模板参数列表中除了类型参数之外的参数,例如整数、指针等。这些参数在编译时必须是常量表达式。例如:

template<typename T, int N>
class MyArray {
public:
    T arr[N];
};
```在这个例子中,`N`是一个非类型参数表示数组的大小使用时需要提供一个常量表达式作为参数例如
```cpp
MyArray<int, 10> myArray; // 创建一个包含10个整数的数组
非类型参数可以用于控制模板的行为,例如指定数组的大小、控制循环次数等。需要注意的是,非类型参数必须是编译时常量,这意味着它们必须在编译时能够确定其值。

模板类型参数和非类型参数都可以有默认值。例如:

template<typename T = int, int N = 10>
class MyArray {
public:
    T arr[N];
};
在这个例子中,如果用户在实例化MyArray时没有提供模板参数,编译器会使用默认值int10

模板参数可以是模板。例如,如果我们想把std::vector作为一个模板参数,可以这样定义:

template<template<typename> class Container, typename T>
class MyClass {
public:
    Container<T> data;
};
在这个例子中,Container是一个模板参数,它接受一个类型参数T。我们可以使用MyClass来创建一个包含std::vector<int>的实例:
MyClass<std::vector, int> myClass;

类型转换

为了使两个不相关的类可以进行隐式转换,我们需要定义转换构造函数或转换运算符。转换构造函数是指一个类的构造函数接受一个参数,并且没有使用explicit关键字修饰,这样就允许编译器在需要该类型的地方自动调用这个构造函数进行类型转换。例如:

class A {
public:
    A(int x) {
        // 构造函数实现
    }
};
class B {
public:
    operator A() {
        // 转换运算符实现
        return A(0); // 返回一个A类型的对象
    }
};
在这个例子中,类A有一个接受int参数的构造函数,而类B定义了一个转换运算符,可以将B类型的对象转换为A类型的对象。当我们需要将一个B类型的对象赋值给一个A类型的变量时,编译器会自动调用这个转换运算符进行类型转换。

拷贝构造函数是指一个类的构造函数接受一个同类型的对象作为参数,用于创建一个新的对象并将其初始化为传入对象的副本。拷贝构造函数是转换构造函数的一种特殊情况

为了避免显式转换带来的问题,C++提供了四种类型转换运算符:static_castdynamic_castconst_castreinterpret_cast。这些运算符提供了更安全和明确的类型转换方式,避免了隐式转换可能引起的错误。 - static_cast:用于进行基本类型之间的转换,例如从int转换为float,或者从一个类类型转换为另一个类类型(如果存在合适的转换构造函数或转换运算符)。static_cast编译时进行检查,如果转换不合法会产生编译错误。

  1. static_cast既可以用于类和派生类的向下转换,也可以用于向上转换。
  2. static_cast仅在编译时进行类型检查,不会进行运行时检查,因此在进行向下转换时需要确保转换是合法的,否则可能会导致未定义行为。
  3. 静态转换不要求基类必须具有虚函数,因此可以用于非多态类型之间的转换。
  • dynamic_cast:用于进行多态类型之间的转换,主要用于将基类指针或引用转换为派生类指针或引用dynamic_cast在运行时进行检查,如果转换不合法会返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。

    使用dynamic_cast时需要注意: 1. 只有当基类具有至少一个虚函数时,dynamic_cast才有效。 2. dynamic_cast只能用于指针或引用类型,不能用于基本类型。 3. 当转换失败时,dynamic_cast会返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用),因此在使用dynamic_cast时需要进行适当的错误处理。 4. dynamic_cast依赖于RTTI(Run-Time Type Information)机制,RTTI信息在编译时生成,需要在编译器选项中启用RTTI支持。

  • const_cast:用于去除对象的constvolatile属性const_cast只能用于指针或引用类型,不能用于基本类型。如果尝试使用const_cast去除一个对象的const属性,并且该对象原本是一个常量,那么对该对象的修改将导致未定义行为。

    主要用于调用一些需要非const参数的函数,但我们只有一个const对象的指针或引用时,可以使用const_cast来去除const属性。

  • reinterpret_cast:用于进行低级别的类型转换,例如将一个指针转换为一个整数,或者将一个整数转换为一个指针。reinterpret_cast不进行任何类型检查,因此使用时需要非常小心,避免引起未定义行为。

类与成员

override和final

overridefinal是C++11引入的两个关键字,用于增强类的继承和多态性。 - override:用于标识一个成员函数是重写(override)基类中的虚函数。当我们在派生类中重写基类的虚函数时,使用override关键字可以让编译器检查我们是否正确地重写了基类的函数。如果我们没有正确地重写(例如,函数签名不匹配),编译器会产生一个错误。这有助于避免由于拼写错误或参数类型不匹配而导致的意外行为。

class Base {
public:
    virtual void foo() {
        cout << "Base::foo" << endl;
    }
};
class Derived : public Base {
public:
    void foo() override { // 正确重写了Base::foo
        cout << "Derived::foo" << endl;
    }
};
- final:用于标识一个成员函数或一个类不能被进一步重写或继承。当我们在一个成员函数上使用final关键字时,表示这个函数不能被派生类重写;当我们在一个类上使用final关键字时,表示这个类不能被继承。这有助于防止意外的重写或继承,增强代码的安全性和可维护性。
class Base {
public:
    virtual void foo() final { // 这个函数不能被重写
        cout << "Base::foo" << endl;
    }
};

静态成员

静态成员是指属于类而不是属于某个对象的成员。静态成员在内存中只有一份,无论创建多少个对象,所有对象共享同一个静态成员。静态成员可以是变量、函数或嵌套类型。 静态成员变量需要在类外进行定义和初始化。例如:

class MyClass {
public:
    static int count; // 声明静态成员变量
};
int MyClass::count = 0; // 定义和初始化静态成员变量
对于静态函数,则只能访问静态成员变量和其他静态函数,不能访问非静态成员变量或非静态函数。

const成员函数

const成员函数是指在成员函数的声明后面加上const关键字,表示该函数不会修改对象的状态(即不会修改成员变量)。在const成员函数中,this指针被隐式地转换为一个指向常量对象的指针,因此不能通过this指针修改对象的成员变量。

class MyClass {
public:
    int value;
    void setValue(int v) {
        value = v; // 可以修改成员变量
    }
    int getValue() const {
        return value; // 不能修改成员变量
    }
};
const对象只能调用const成员函数,因为const对象的状态不能被修改,而非const成员函数可能会修改对象的状态,因此不能被const对象调用。

子类访问父类函数

当子类需要访问父类的函数时,可以使用作用域解析运算符::来指定要调用的父类函数。例如:

class Base {
public:
    void foo() {
        cout << "Base::foo" << endl;
    }
};
class Derived : public Base {
public:
    void foo() {
        cout << "Derived::foo" << endl;
    }
    void callBaseFoo() {
        Base::foo(); // 调用父类的foo函数
    }
};
在这个例子中,Derived类重写了Base类的foo函数,但在callBaseFoo函数中,我们使用Base::foo()来调用父类的版本。

友元

友元(friend)是C++中的一个特性,允许一个函数或一个类访问另一个类的私有成员和保护成员。友元关系是单向的,即被声明为友元的函数或类可以访问另一个类的私有成员,但反过来不成立。

class MyClass {
private:
    int secret;
public:
    MyClass(int s) : secret(s) {}
    friend void revealSecret(MyClass& obj); // 声明友元函数
};
void revealSecret(MyClass& obj) {
    cout << "The secret is: " << obj.secret << endl; // 访问私有成员
}
在这个例子中,revealSecret函数被声明为MyClass的友元函数,因此它可以访问MyClass的私有成员secret。友元函数可以是普通函数、成员函数或其他类的成员函数。友元关系可以跨越类之间的层次结构,即一个类可以是另一个类的友元,而不需要它们之间存在继承关系。

运算符重载

运算符重载是C++中的一个特性,允许我们为自定义类型定义自己的运算符行为。通过重载运算符,我们可以使自定义类型的对象能够使用常见的运算符进行操作,例如加法、减法、乘法等。

class Complex {
public:
    double real;
    double imag;
    Complex(double r, double i) : real(r), imag(i) {}
    Complex operator+(const Complex& other) {
        return Complex(real + other.real, imag + other.imag);
    }
};
在这个例子中,我们定义了一个Complex类,并重载了加法运算符+,使得我们可以直接使用+运算符来相加两个Complex对象。 重载运算符不一定非得在类内定义,也可以在类外定义。例如:
Complex operator+(const Complex& c1, const Complex& c2) {
    return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
在这个例子中,我们在类外定义了加法运算符+,并且接受两个Complex对象作为参数。

宏定义

宏定义是C++中的一个预处理器指令,用于定义常量、函数或代码片段。宏定义使用#define指令来创建一个宏,宏可以接受参数,也可以不接受参数。 井号#在宏定义中有特殊的作用: - 当宏定义中使用#运算符时,它会将宏参数转换为字符串。例如:

#define TO_STRING(x) #x
在这个例子中,TO_STRING(Hello)会被预处理器替换为"Hello"。 - 当宏定义中使用##运算符时,它会将两个宏参数连接在一起。例如:
#define CONCAT(a, b) a##b
在这个例子中,CONCAT(Hello, World)会被预处理器替换为HelloWorld。 宏定义在编译时进行文本替换,因此它们没有类型检查和作用域限制,使用时需要小心,避免引起意外的行为。建议在可能的情况下使用const变量、函数或模板来替代宏定义,以获得更好的类型安全和可维护性。

要定义多行宏,可以使用反斜杠\来连接多行代码。

C++中有若干预定义宏,例如: - __FILE__:表示当前源文件的名称。 - __LINE__:表示当前行号。 - __DATE__:表示当前编译的日期。 - __TIME__:表示当前编译的时间。 - __cplusplus:表示当前编译器支持的C++标准版本,例如199711、201703等。

可变参数宏是指宏定义中使用了省略号...来表示可接受任意数量的参数。例如:

#define LOG(format, ...) printf(format, __VA_ARGS__)
在这个例子中,LOG宏可以接受一个格式字符串和任意数量的参数,并将它们传递给printf函数进行输出。使用可变参数宏时,需要使用__VA_ARGS__来表示传递给宏的可变参数列表。

智能指针

智能指针是C++11引入的一种资源管理工具,用于自动管理动态分配的内存,避免内存泄漏和悬空指针等问题。智能指针通过重载运算符来提供类似于普通指针的使用方式,同时在对象生命周期结束时自动释放资源。 C++标准库提供了三种主要的智能指针类型:std::unique_ptrstd::shared_ptrstd::weak_ptr

std::unique_ptr

std::unique_ptr是一种独占所有权的智能指针,表示一个对象只能有一个unique_ptr拥有。当unique_ptr被销毁时,它所管理的对象也会被自动释放。unique_ptr不支持复制,但支持移动语义。

移动语义是指资源的所有权可以从一个对象转移到另一个对象,而不是复制资源。通过使用std::move函数,我们可以将一个unique_ptr的所有权转移给另一个unique_ptr,从而避免了不必要的资源复制和内存泄漏。

std::unique_ptr<int> ptr1(new int(5)); // 创建一个unique_ptr
std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移ptr1的所有权给ptr2,此时ptr1不再拥有该对象

它包含下列常用成员函数: - get():返回unique_ptr所管理的原始指针。 - release():释放unique_ptr所管理的对象,并返回原始指针。调用release()后,unique_ptr不再拥有该对象,调用者需要负责释放该对象的内存。 - reset():释放unique_ptr当前管理的对象,并可选择性地将其替换为一个新的对象。例如,reset(new T())会释放当前对象并管理一个新的对象。 - swap():交换两个unique_ptr的所有权。例如,ptr1.swap(ptr2)会交换ptr1ptr2所管理的对象。

我们可以用std::make_unique函数来创建一个unique_ptr,它提供了更安全和简洁的方式来创建智能指针。例如:

auto ptr = std::make_unique<int>(5); // 创建一个unique_ptr,管理一个值为5的整数对象

std::shared_ptr

std::shared_ptr是一种共享所有权的智能指针,表示一个对象可以有多个shared_ptr拥有。当最后一个拥有该对象的shared_ptr被销毁时,该对象才会被自动释放。shared_ptr支持复制和移动语义。 它包含下列常用成员函数: - get():返回shared_ptr所管理的原始指针。 - use_count():返回当前有多少个shared_ptr实例共享所有权。 - unique():返回一个布尔值,表示当前shared_ptr是否是唯一拥有该对象的实例。 - reset():释放shared_ptr当前管理的对象,并可选择性地将其替换为一个新的对象。 - swap():交换两个shared_ptr的所有权。

我们可以用std::make_shared函数来创建一个shared_ptr

std::weak_ptr

std::weak_ptr是一种弱引用的智能指针,表示一个对象可以被多个shared_ptr拥有,但weak_ptr本身不拥有该对象。当最后一个拥有该对象的shared_ptr被销毁时,该对象会被自动释放,而所有指向该对象的weak_ptr会变为无效。weak_ptr主要用于解决循环引用的问题。 它包含下列常用成员函数: - lock():返回一个shared_ptr,如果原始对象仍然存在,则该shared_ptr拥有该对象;如果原始对象已经被销毁,则返回一个空的shared_ptr一般我们需要获取weak_ptr所管理的对象时,首先调用lock()函数来获取一个shared_ptr,然后检查该shared_ptr是否为空,以确定原始对象是否仍然存在。此外,lock()为原子操作,可以在多线程环境中安全地使用。

std::weak_ptr<int> weakPtr; // 创建一个weak_ptr
{
    auto sharedPtr = std::make_shared<int>(5); // 创建一个shared_ptr,管理一个值为5的整数对象
    weakPtr = sharedPtr; // weak_ptr现在指向shared_ptr管理的对象
} // sharedPtr超出作用域被销毁,此时weakPtr变为无效
auto lockedPtr = weakPtr.lock(); // 尝试获取shared_ptr
if (lockedPtr) {
    // lockedPtr不为空,原始对象仍然存在
    cout << *lockedPtr << endl; // 输出5
} else {
    // lockedPtr为空,原始对象已经被销毁
    cout << "Object has been destroyed." << endl;
}
- expired():返回一个布尔值,表示原始对象是否已经被销毁。 - reset():释放weak_ptr当前管理的对象,并可选择性地将其替换为一个新的对象。 - swap():交换两个weak_ptr的所有权。

weak_ptr是如何解决循环引用的问题的? 循环引用是指两个或多个对象相互持有对方的shared_ptr,导致它们的引用计数永远不会降为零,从而无法自动释放内存。通过使用weak_ptr,我们可以打破这种循环引用。例如,如果对象A持有一个shared_ptr指向对象B,而对象B持有一个weak_ptr指向对象A,那么当对象A被销毁时,对象B的weak_ptr会变为无效,而不会阻止对象A的销毁,从而避免了内存泄漏的问题。

Lambda表达式

Lambda表达式是C++11引入的一种匿名函数,可以直接在需要函数对象的地方定义和使用。Lambda表达式的语法如下:

[捕获变量](参数列表) 可选限定符-> 返回类型 {
    // 函数体
}
例如:
int a = 3;
auto addA = [a](int x) -> int {
    return a + x;
};
cout << addA(5) << endl; // 输出8
捕获变量包括值捕获和引用捕获两种方式。值捕获会将变量的值复制到Lambda表达式中,而引用捕获会将变量的引用传递给Lambda表达式,使得Lambda表达式可以修改外部变量的值。
int a = 3;
auto addA = [&a](int x) -> int {
    a += x; // 修改外部变量a的值
    return a;
};
cout << addA(5) << endl; // 输出8
cout << a << endl; // 输出8,说明外部变量a的值被修改了
类变量/结构体变量在被值捕获时会调用拷贝构造函数。若拷贝构造函数被删除或不可访问,则会导致编译错误。

在一个类中,如果我们想在成员函数中使用Lambda表达式,并且需要捕获类的成员变量,我们可以使用this指针来捕获整个对象。例如:

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}
    void printValue() {
        auto lambda = [this]() {
            cout << "Value: " << value << endl; // 捕获整个对象,访问成员变量
        };
        lambda();
    }
};

使用按值捕获时,默认无法修改捕获的变量,但可以使用mutable关键字来允许修改捕获的变量。例如:

int a = 3;
auto addA = [a]() mutable -> int {
    a += 5; // 修改捕获的变量a的值
    return a;
};
cout << addA() << endl; // 输出8
cout << a << endl; // 输出3,说明外部变量a的值没有被修改

左值与右值

左值(lvalue)是指一个表达式所表示的对象具有一个持久的地址,可以被赋值或取地址。右值(rvalue)是指一个表达式所表示的对象没有持久的地址,通常是一个临时对象或字面值,不能被赋值或取地址。

int x = 5; // x是一个左值,5是一个右值
int* ptr = &x; // 可以取地址,ptr指向x
带有const修饰的左值被称为常量左值(const lvalue),它们不能被修改,但仍然具有持久的地址。

在C++11后,左右值在宏观上被分为glvalue(generalized lvalue)和rvalue两大类。glvalue包括左值和将亡值(xvalue),而右值则包括纯右值(prvalue)和将亡值(xvalue)。

将亡值(xvalue)是指一个表达式所表示的对象即将被销毁,可以被移动语义所利用。例如,std::move函数会将一个左值转换为一个将亡值,以便触发移动构造函数或移动赋值运算符。

std::string str = "Hello";
std::string movedStr = std::move(str); // str被转换为一个将亡值,触发移动构造函数
纯右值(prvalue)是指一个表达式所表示的对象没有持久的地址,通常是一个临时对象或字面值。例如,字面值5、函数返回的临时对象等都是纯右值。
int getValue() {
    return 5; // 返回一个纯右值
}

此外,表达式也是纯右值。例如m+n,它的计算结果是一个临时对象,没有持久的地址,因此是一个纯右值。显式转换表达式也是纯右值。例如(int)x

这五个概念的关系图为: