Skip to content

C++入门指南

更新: 4/19/2025   字数: 0 字   时长: 0 分钟

数据类型

基础数据类型

注意:以下数据类型的位数以64位操作系统为准。

基础数据类型JavaC#C++
布尔类型bool
8位字符类型xchar(取值范围:[-128, 127]或[0, 255])
8位有符号字符类型signed char(取值范围:[-128, 127])
8位无符号字符类型unsigned char(取值范围:[0, 255])
16位Unicode字符类型charchar16_t
32位Unicode字符类型xchar32_t
宽字符类型xwchar_t(2或4字节,存储Unicode字符)
8位有符号整数bytesbytebyte
16位有符号整数short
32位有符号整数int
64位有符号整数longlong [long]
32位单精度浮点类型float
64位双精度浮点类型double
扩展精度浮点类型xlong double(8、12或16字节)
精准浮点类型xdecimalx
8位无符号整数sbyteunsigned byte
16位无符号整数ushortunsigned short
32位无符号整数uintunsigned int
64位无符号整数ulongunsigned long [long]
  • Java没有有无符号之分,都是有符号的,取值范围从负数到正数。
  • C++的long类型可能是4字节或8字节,具体取决于平台,但long long一定是8字节。

复合数据类型

数组

  • 数组名表示数组首元素的地址,而&数组名表示整个数组的首地址。虽然值相同,但含义不同。
  • 数组名可看做是一个指向不变的指针(相关内容见const关键字)。
c++
int arr1[3] = {1, 2, 3};
cout << "arr1  = " << arr1 << endl;
cout << "&arr1 = " << &arr1 << endl;
cout << "arr1 + 1 = " << arr1 + 1 << endl;  // 第二个元素地址,相差4个字节(int类型)
cout << "&arr1 + 1 = " << &arr1 + 1 << endl;  // 相差整个数组长度的字节数,即3*4字节
cout << "*(arr1 + 1) = " << *(arr1 + 1) << endl;  // 第二个元素的值

字符数组

字符数组是C风格的字符串。

  • 字符数组末尾需要有结束标识符号'\0'。
  • 字符数组是可以修改的。
c++
// 系统自动在末尾加上'\0',所以数组长度为4
char name1[] = "dog";  
// {}形式初始化,系统不会在末尾加上'\0',需要自己加上。
char name2[] = { 'd', 'o', 'g', '\0'};  
// 字符串长度小于数组长度,数组剩余空间全被初始化为'\0'
char name3[10] = "dog";
cout << name1 << " " << strlen(name1) << " " << sizeof(name1) << endl; // 3 3 4 
cout << name2 << " " << strlen(name2) << " " << sizeof(name2) << endl; // 3 3 4 
cout << name3 << " " << strlen(name3) << " " << sizeof(name3) << endl; // 3 3 10

name3[3] = 's';
name3[5] = 'x';
cout << "modified name3 = " << name3 << endl;  // dogs

提示

在获取了字符串的长度后,切记长度+1来初始化字符数组的长度!

指针

指针是一个变量,其存储的是值的地址,而不是值本身。

c++
int x = 10;
int* ptr = &x;
int** pptr = &ptr;  // 指针是变量,所以它也有地址,pptr是二级指针,即指向指针的指针。
// &x表示x的地址,即ptr变量的值。
cout << "&x = " << x << ", ptr = " << ptr << endl;
// *运算符用于指针表示解除引用,可以获取指针所指向的地址空间的值。即x = *ptr。
cout << "x = " << x << ", *ptr = " << *ptr << endl;

指针多用于使用new在堆中动态分配内存,如:

c++
int* p1 = new int(10);
int* p2 = new int[10];
// do something;
delete p1; // 释放内存
delete[] p2;  // 使用new[]为数组分配内存,则应使用delete[]释放内存。
  • new分配的内存用delete来释放;new[]为数组分配的内存用delete[]来释放。
  • 不要使用delete来释放不是new分配的内存
  • 不要使用delete释放同一内存块两次。
  • 对空指针应用delete是安全的
常见指针问题
  • 野指针:未被初始化或已经被释放的指针,其指向的内存地址是未知的。声明指针时要初始化为nullptr或有效的地址。
  • 悬挂指针:指针指向的内存空间已经被释放,但指针仍然指向该内存空间。新手常见的错误是函数返回了局部变量(栈空间)的指针。
    c++
    static int* buildX(int n=1)
    {
      int x = 10 * n; // 在栈空间上分配的内存,函数执行完之后将被释放。
      cout << "&x=" << &x << endl;
      return &x;
    }
    static int* buildY(int n=1)
    {
      int* y = new int(10 * n);  // 在堆空间上分配的内存,由程序员自己释放。
      cout << "y=" << y << endl;
      return y;
    }
    int main()
    {
      int* xp = buildX(10); // xp是悬挂指针,所指向的内存空间已经被释放
      cout << "xp=" << xp << ", *xp=" << *xp << endl;
      int* yp = buildY(10);
      cout << "yp=" << yp << ", *yp=" << *yp << endl;
      delete yp;  // 释放指针所指向的内存空间,但指针本身的值还是指向该内存空间。
      yp = nullptr;  // 避免悬挂指针
    }
指向函数的指针

最简形式:void (*p_func)()

较为复杂的函数指针数组:

c++
const double* (*pf[3])(const double* arr, int n) = { func1, func2, func3 };

const double* (*(*pf2)[3])(const double* arr, int n) = &pf;
解读

首先要知道运算符的优先级:() > [] > *

引用【左值引用】

引用是已定义的变量的别名,其主要用途是作为函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本,提高效率。

c++
int a = 10;
int &ra = a;  // ra是a的引用
int* pa = &a; // pa是a的指针

提示

必须在声明引用变量时进行初始化,并且不能重新绑定到另一个变量。

枚举

C++11引入了增强型枚举(传统枚举就不要用了),枚举值限定在类作用域内,避免了命名冲突。

c++
enum class State : int
{
	Unknown = 0,
	Success = 1,
	Failed = 2,
	Running = 3
};

auto state = State::Running;

结构体

C++的结构体和类基本相同,唯一的区别是默认的访问权限:结构体成员和继承的默认访问权限都是public;而类成员和继承的默认访问权限都是private。

结构体主要用于对数据的简单封装,较为复杂的业务逻辑应该使用类。

c++
struct Point
{
    float x;
    float y;

    void display() const
    {
        cout << "(" << x << ", " << y << ")" << endl;
    }
};

void test_struct()
{
    Point point = {1.0, 2.5};
    point.display();
}

联合体

多个成员共享同一块内存。

标准库数据类型

string

string类是对字符数组的封装,支持自动扩容,提供了更加简单的API。

pair

std::pair<T1, T2>是一个模板类,适用于需要将两个元素(可以不同类型)组合在一起的场景。

c++
#include <utility>
using namespace std;

std::pair<int, char> tuple(1, 'a');
cout << "first val:" << tuple.first << endl;
cout << "second val:" << tuple.second << endl;

tuple

元组是pair的泛化,支持任意数量的元素组合。用法比较奇怪,用的时候去查吧!

容器

容器是存储其它对象的对象,储存的对象类型必须是可复制构造的和可赋值的。

序列容器
基本容器描述内存结构固定大小使用场景
array固定数组连续存储数组大小固定的随机索引访问
vector动态数组连续存储动态扩容且随机索引访问
list双向链表非连续存储双向遍历,随机插入或删除元素
forward_list单向链表非连续存储单向遍历,随机插入或删除元素
deque双端队列分段连续内存头尾两端频繁插入或删除元素

容器适配器是基于基本容器的简化接口,它们提供了一种特定的接口来访问底层容器的数据,其底层容器通常可以有多种选择。

容器适配器描述特点默认底层容器
stack后进先出deque
queue队列先进先出deque
priority_queue优先级队列快速访问优先级最高的元素vector实现的最大堆
关联容器

基于键值对的容器,它们通过键来存储和访问元素。关联容器分为集合和映射,集合使用红黑树(对数级时间复杂度),映射使用哈希表(常数级时间复杂度)。

关联容器描述
set有序集合
unordered_set无序集合
multipset允许重复值的有序集合
unordered_multiset允许重复值的无序集合
map有序映射
unordered_map无序映射
multipmap同一个键可关联多个值的有序映射
unordered_multimap同一个键可关联多个值的无序映射

数据类型转换

类型转换描述
静态类型转换static_cast将一种数据类型的值强制转换为另一种近似的数据类型的值。
动态类型转换dynamic_cast通常用于将一个基类指针或引用转换为派生类指针或引用。指针类型转换失败返回空指针;引用类型转换失败会抛出std::bad_cast异常
常量转换
const_cast
将const类型的对象转换为非const类型的对象,不改变对象类型。
重新解释转换
reinterpret_cast
将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。

关键字

const

定义常量

非const变量默认是extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。

c++
// 不指定extern,则只能在当前文件内访问
extern const int BUF_SIZE = 100;

int main() {
  const int n = 10; // const变量,不能修改,且必须初始化
}

const与指针

const位于*左侧,修饰的是* 变量,修饰的是值,即不可以通过指针来修改其指向的值,但可以修改指针的指向。

c++
const char* s1 = "Tom";
// *s1 = "Lucy";  // 错误,不能通过指针来修改其指向的值
s1 = "Lucy"; // 修改指针的指向

这里并不是修改了Tom,而是在新的内存中存放Lucy,把指针s1的指向由Tom的内存改为指向Lucy的内存。

const位于*右侧,修饰的是指针变量,指针的值是不可以改变的,即指向不能变,必须初始化。可以通过指针来修改其指向的值。

c++
char* const s2 = "Tom";
// *s2 = "Lucy"; // 错误
// s2 = "Lucy";  // 错误,因为指向不可改变

为什么高亮的行也会错误呢?在C++中,字符串字面量的类型是const char[],是不可以修改的。事实上,这相当于上面两者的结合体,既不能修改指针指向,也不能通过指针修改其指向的值。

c++
const char* const s2 = "Tom";

如果要修改,应该使用string或字符数组。

const与函数

static

  1. 静态函数:静态函数的作用域为当前文件,即只能在当前文件内访问。普通函数默认是extern,可以在其他文件中访问。
  2. 静态变量:空间分配只初始化一次,存在于程序运行的整个生命周期。
  3. static与类:静态成员变量和静态成员方法都由类所拥有,内存空间只有一份,类的所有实例对象都共享类的静态成员变量和静态成员方法。
    • 静态成员变量:不能使用构造函数初始化,除非静态成员变量是const整数类型或枚举型,否则不能在声明时初始化。
    • 静态成员方法:只能访问静态成员变量和静态成员方法
    • 可直接通过类名访问public的静态成员变量和静态成员方法。
c++
static void test2() {
	static int age = 1; // age变量在静态存储区,再次运行函数也不会重新初始化age
	cout << "age = " << age << endl;
	age++;
	cout << "age = " << age << endl;
}
c++
class Person 
{
  public:
    static int age;
    static void addAge() {
      age++;
    }
}

int Person::age = 1;

int main() {
  Person p1 = Person();
  p1.addAge();
  Person::addAge();
  cout << "age = " << Person::age << endl;  // 3
  Person p2 = Person();
  p2.addAge();
  cout << "age = " << p2.age << endl;  // 4
}

extern

  1. 用于声明全局变量。通常用于在多个源文件中共享全局变量。
  2. 用于解决 C++ 和 C 代码之间的兼容性问题。由于C++支持函数重载,C++编译器会将函数名与参数类型、返回值等信息一起编译到函数符号中,而C编译器不会这样做。因此,直接在C程序中调用C++函数会导致链接错误。为了解决这个问题,可以使用extern "C"关键字来指示编译器按照C语言的方式来处理特定的代码。
    c++
    //xx.h
    int add(...)
    //xx.c
    int add(){}
    
    //xx.cpp
    extern "C" {
      #include "xx.h"
    }
    c++
    //xx.hpp 
    #include <iostream>
    extern "C" {
      // 可同时声明多个函数
      int add(int, int);
      void increment();
    }
    //xx.cpp 
    int add(int x, int y)
    {
      std::cout << "x=" << x << ", y=" <<y << std::endl;
      return x + y;
    }  
    
    //xx.c 先声明函数,再调用
    extern int add(int, int);  // extern可省略,但推荐加上
    
    int main()
    {
      add(1, 2);
    }

inline

  • 在类中声明函数并定义,则该函数是隐式内联函数。在声明之后想要成为内联函数,必须在定义处加inline关键字。(推荐后者写法)
  • 虚函数可以是内联函数,但当虚函数表现多态性的时候不会内联,因为虚函数的多态性在运行期,而编译器无法知道运行期调用哪块代码。编译器需要知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

内联函数仅仅省去了函数调用的开销,从而提高函数的执行效率,但每一处的内联函数都要复制代码,会导致程序代码量增加。以下情况不适合使用内联:

  1. 函数体内的代码较长,内联将导致内存消耗代价较高。
  2. 执行函数的时间比调用函数的开销大。

final

  • 将类声明为final,将禁止此类被继承。
  • 普通函数不能声明为final,只有虚函数声明为final,可防止派生类覆盖该虚函数。

override

显式声明派生类中的成员函数覆盖了基类中的虚函数。

  • 增强可读性并进行编译器检查:如果派生类中的函数没有正确覆盖基类中的虚函数(例如函数签名不匹配),编译器会报错。
  • 与final结合使用表示该函数覆写了基类的虚函数,且不允许进一步覆写。

volatile

  • 变量可能被某些编译器未知因素更改,使用volatile修饰变量告知编译器不要对该变量进行优化。
  • volatile声明的变量,每次访问都必须从内存中取值(没有被volatile修饰的变量可能由于编译器的优化,从CPU寄存器中取值)。
  • const可以是volatile(如只读的状态寄存器)
  • 指针可以是volatile

constexpr

  • constexpr修饰的变量必须能在编译阶段确定或计算出。
  • constexpr修饰的函数的参数和返回值(返回值可以用非常量的变量存储)必须满足常量表达式的要求。
  • constexpr结合模板使用可以实现复杂的编译时计算,提高代码的灵活性和性能。
    c++
    template <typename T>
    T getA(T a)
    {
        // std::is_same 编译时检查两个类型是否相同
        if constexpr (std::is_same<T, int>::value)
            return a * 10;
        else if constexpr (std::is_same<T, float>::value)
            return a * 1000;
        else
            return a;
    }

explicit

  • 修饰构造函数时,可以防止隐式转换和复制初始化。
  • 修饰转换函数时,可以防止隐式转换,但按语境转换除外。

函数

函数参数

参数类型函数不需要修改实参函数需要修改实参
基本数据类型值传递指针(推荐)或引用
数组const指针指针
结构体const指针或const引用引用或指针
类对象const引用(推荐)或const指针引用(推荐)或指针

以上只是指导建议,实际情况很可能会有不同的选择。如对于基本类型,cin使用引用,因此可以使用cin>>n,而不是cin>>&n

函数重载

同一作用域内,函数名相同,但函数参数的类型或个数或顺序不同。重载的函数可以有不同的返回类型, 但是仅仅函数返回类型不同不是重载。

函数模板

函数模板使用泛型来定义函数,通过将类型作为参数传递给模板,可使编译器生成该类型的函数。

如果要将同一算法用于不同形参类型的函数,应该使用函数模板。比如有个函数交换两个值,参数类型有整型、浮点型等,如果为每种类型定义一个函数,代码就会很冗余。

c++
// 隐式实例化:常规函数模板
template <typename T> void Swap(T &a, T &b);

// 显式实例化:根据上面的函数模板生成double类型的函数定义。
// 意义不是很大,本身传入double类型的参数时编译器就会生成double类型的函数定义。
template void Swap(double &a, double &b);

template <typename T>
void Swap(T &a, T &b) {
    T temp;
    temp = a;
    a = b;
    b = temp;
}

但并非所有的类型都使用相同的算法,这时可以使用显式具体化——对特定类型,不使用函数模板来生成函数定义,而使用专门为此类型显式地定义的函数定义。

c++
struct job {
  char name[40];
  double salary;
  int floor;
};
// 显示具体化
template <> void Swap<job>(job& j1, job& j2);  // <job>可以省略

template <> void Swap(job& j1, job& j2) {
    double t1;
    int t2;
    t1 = j1.salary;
    j1.salary = j2.salary;
    j2.salary = t1;
    t2 = j1.floor;
    j1.floor = j2.floor;
    j2.floor = t2;
}

如果函数模板的参数有变化,这时可以重载函数模板。

c++
// 重载函数模板
template <typename T> void Swap(T a[], T b[], int n);

template <typename T>
void Swap(T a[], T b[], int n) {
    T temp;
    for (int i = 0; i < n; i++) {
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}

提示

重载是因为函数参数不同,显式具体化是因为算法不同。

面向对象

特殊成员函数

构造函数

  1. 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。如果有定义构造函数,又想使用默认构造函数,则必须显式定义默认构造函数。
  2. 默认构造函数只能有一个,而构造函数可以有多个。
  3. 默认构造函数可以没有参数,如果有,则必须给所有参数都提供默认值。

析构函数

  1. 如果没有定义,编译器提供默认析构函数。

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。其原型如下:

c++
ClassName(const ClassName &);

如果没有定义,编译器会提供默认复制构造函数,其行为是逐个拷贝非静态成员变量(成员复制也称为浅复制),复制的是成员的值。

什么情况下会调用复制构造函数?

  1. 新建一个对象并将其初始化为同类型现有对象时。
    c++
    // 假设Person类,已有实例对象p1
    Person p2(p1);
    Person p2 = p1;
    Person p2 = Person(p1);
    Person* ptr = new Person(p1);
  2. 生成对象副本时,如函数按值传递对象或函数返回对象。

赋值运算符

通过重载赋值运算符实现类对象赋值,其原型如下:

c++
ClassName & ClassName::operator=(const ClassName &);

何时会调用赋值运算符?将已有对象赋给另一个对象时。

如果没有定义,编译器会提供默认赋值运算符,其行为同复制构造函数。

在构造函数中使用new时要特别小心

  1. 构造函数中使用new初始化指针成员,则析构函数中应该使用delete。
  2. 如果有多个构造函数,则必须以相同方式使用new,要么都带中括号,要么都不带。因为析构函数只有一个,newdeletenew[]delete[]要对应。
  3. 应该定义一个复制构造函数,通过深拷贝将一个对象初始化为另一个对象。
  4. 应该重载赋值运算符,通过深拷贝将一个对象复制给另一个对象。

函数返回对象的情况

返回对象

当函数返回的对象是局部变量时,则不应该按引用方式返回,因为函数执行完后局部变量将调用析构函数,引用指向的对象将不再存在。通常,被重载的运算符会返回对象。

这种情况下,存在调用复制构造函数来创建被返回的对象的开销,但这是不可避免的。

返回const对象

意义不大。

返回const引用

返回指向const对象的引用,通常是为了提高效率,但有前提:函数返回的是传参的对象,参数也应该是const引用。如比较两个对象:

c++
const Person& Max(const Person& p1, const Person& p2)
{
  if (p1.age > p2.age)
    return p1;
  else
    return p2;
}

返回非const引用

两种常见的情形:

  1. 重载赋值运算符,旨在提高效率。一般来说,对象都是可修改的,所以返回不会加const。
  2. 重载与cout一起使用的<<运算符。这种情形只能返回非const引用ostream &,如果返回类型是ostream,将要求调用ostream类的复制构造函数,而ostream类没有公有的复制构造函数。

封装

访问修饰符JavaC#C++
public都可见
protected同一包内的类本身和派生类可见类本身和派生类可见
private只对类本身可见
缺省同一包内可见类是internal,成员是privateprivate
  • 类的实例对象不属于类本身。
  • C#有5个访问修饰符,可指定7种可访问性级别。
  • C++的访问修饰符只能修饰成员,不能修饰类;而Java和C#的修饰符能修饰类和类的成员。

继承

访问控制

C++类继承时可以指定继承的访问级别。

基类成员/继承方式privateprotectedpublic
private
protectedprivateprotectedprotected
publicprivateprotectedpublic
  • 基类private成员,不管何种方式继承,派生类都无法访问。
  • 基类protected成员,派生类都可以访问。
    • private继承:其基类的protected成员在派生类中是private成员。
    • protected或public继承:其基类的protected成员在派生类中是protected成员。
  • 基类public成员,派生类都可以访问
    • private继承:其基类的public成员在派生类中是private成员。
    • protected继承:其基类的public成员在派生类中是protected成员。
    • public继承:其基类的public成员在派生类中是public成员。

基类方法

  • 派生类继承了所有的基类方法,但以下除外:
    • 基类的构造函数、析构函数、复制构造函数、赋值运算符
    • 基类的重载运算符
    • 基类的友元函数
  • 派生类需要定义自己的构造函数。创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
  • 基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象,但只能调用基类方法

多态

同一个方法在不同的派生类中有不同的行为。

实现方式:

  1. 在派生类中重新定义基类的方法,通过不同的派生类调用方法。
  2. 使用虚函数,但有前提条件:
    • 派生类必须对基类的虚函数进行重写,重写的函数可以继续声明为虚函数。
    • 必须通过基类的指针(或引用)调用虚函数,但实际给的是派生类的指针(或引用)。

对于方式1,编译器对非虚函数使用静态联编,在编译阶段根据指针(或引用)的类型,调用类型对应的函数。

对于方式2,编译器对虚函数使用动态联编,在代码执行时根据指针(或引用)所指向的类型(注意不是指针的类型),调用对应的函数。

c++
class Animal
{
  public:
    virtual void speak(const string& s = "...")
    {
      cout << "animal speak " << s << endl;
    }
}
class Dog : public Animal
{
  public:
    void speak(const string& s = "wang")
    {
      cout << "dog speak " << s << endl;
    }
}
class Cat : public Animal
{
  public:
    void speak(const string& s="miao")
    {
      cout << "cat speak " << s << endl;
    }
}
c++
void test1()
{
  Dog* dog = new Dog();
  dog->speak();
  Cat* cat = new Cat();
  cat->speak();
}
c++
void speak(const Animal& animal)
{
  animal.speak();
}
void test2()
{
  Animal* animal = new Dog();
  // 通过基类调用虚函数,实际调用的是派生类的虚函数;
  // 但虚函数的默认参数使用的是基类的默认参数
  animal->speak();  
  // 更常见的方式
  Dog* dog = new Dog();
  Cat* cat = new Cat();
  speak(*dog);
  speak(*cat);
}

虚函数

  • 默认参数是静态绑定的(编译阶段已经确定),而虚函数是动态绑定的。所以虚函数的默认参数取决于指针或引用的类型,而不是它们所指向的对象的类型。
  • 给基类提供一个虚析构函数准没错。
  • 构造函数、静态函数不能是虚函数。
  • 友元不能是虚函数,因为友元不是类成员。

提示

对于Java和C#,它们没有虚函数的概念,通过重写实现多态。

抽象基类

C++使用纯虚函数提供未实现的函数。包含有纯虚函数的类即为抽象基类,不能创建抽象基类的实例对象。

c++
virtual void speak() const = 0;  // =0 表示纯虚函数
  • 一个类继承抽象基类,它必须实现抽象基类的所有纯虚函数,才能称为非抽象类。
  • 抽象基类可以有构造函数。

类模板

友元

接口

C++新特性

右值引用

  • 左值和右值的区别在于能否取地址。左值能通过&获取地址,而右值不能。
  • 左值可以出现在=的左边或右边,但右值只能出现在=的右边。
  • 左值包括变量、数组元素、结构体成员等等。右值包括字面常量、诸如x+y等表达式、返回值不是引用的函数等。

右值引用可关联到右值,即可出现在赋值表达式右边但不能对其应用地址运算符的值。

c++
int a = 10; // a是左值,10是右值。
int b = a;
int&& rr1 = 100;  // rr1是右值引用
int&& rr2 = a + b;  // rr2是右值引用

引入右值引用的主要目的之一是实现移动语义。

移动语义

Lambda

表达形式:[]() mutable -> returntype {};

语法描述
[]捕获列表:捕获外部变量供函数体使用。不可省略
[=]值传递方式捕获函数体内使用的外部变量;
[&]引用传递方式捕获函数体内使用的外部变量。
()参数列表:lambda函数的参数。
mutable修饰符:允许在函数体中修改捕获的变量。
因为引用传递方式就可修改外部变量,所以基本不用。
-> returntype返回类型:lambda函数的返回类型。
{}函数体:lambda的函数体。 不可省略
  • 如果使用了 mutable-> returntype,则不能省略(),即使没有参数。
  • 按值传递捕获的变量不受外部修改的影响,函数体内还是原值;按引用传递捕获的变量外部修改会影响函数体内的变量的值。
  • 虽然返回值类型编译器可以推导,但最好还是注明返回值类型。
c++
auto l2 = [] {};  // 最简形式

auto add2 = [](int x, int y) -> int {return x + y;};
cout << add2(1, 2) << endl;

auto a(10), b(20);
auto add3 = [=, &b](int z) -> int {return a + b + z;};
a = 20;
b = 5;
cout << add3(10) << endl;  // 25

智能指针

智能指针类型描述
auto_ptrC++98定义的智能指针模板,C++11后被淘汰了。
unique_ptr独占所有权的智能指针,同一内存只能有一个 unique_ptr 指针。
shared_ptr共享所有权的智能指针,多个 shared_ptr 可以指向同一内存。
采用引用计数,当复制或拷贝时,计数+1;当析构时计数-1,计数为0则释放内存。
weak_ptr弱引用智能指针,用于与 shared_ptr 配合使用,避免循环引用导致的内存泄漏。

初始化

注:xxx_ptr 表示 unique_ptrshared_ptr

c++
// 初始化一个可以指向T类型的空的智能指针
xxx_ptr<T> ptr1;

// 初始化一个指向T类型实例的智能指针
xxx_ptr<T> ptr2(new T());

// 初始化一个可以指向T类型数组的空的智能指针
xxx_ptr<T[]> ptr3; 

// 初始化一个指向T类型数组实例的智能指针
xxx_ptr<T[]> ptr4(new T[]);

// 初始化一个可以指向T类型实例的智能指针,并使用自定义删除器来释放内存
unique_ptr<T, D> ptr5;

// 虽然编译器不会报错,但ptr6无法通过赋值或reset接管对象(运行时异常退出)。
// 要么直接用ptr8的方式,要么就用ptr1的方式并使用ptr1.reset(new T(), D());
shared_ptr<T> ptr6(nullptr, D()); 

// 初始化一个指向T类型实例的智能指针,并使用自定义删除器来释放内存
unique_ptr<T, D> ptr7(new T());
shared_ptr<T> ptr8(new T(), D());

// make_shared 会同时分配控制块(用于引用计数)和对象本身,
// 相比于 new 方式初始化,少一次内存分配次数,更加简洁高效(推荐)
shared_ptr<T> ptr9 = make_shared<T>(args);

unique_ptr

API描述
get()获取接管对象的指针。
reset(...args)销毁当前对象(如果存在),并接管新的对象(如果提供)或重置为空指针。
release()仅释放接管对象的控制权,不会销毁对象。
  • 不支持赋值和复制操作,需要使用std::move()进行移动操作。

shared_ptr

API描述
get()获取接管对象的指针。
reset(...args)当前对象引用计数-1(如果存在),并接管新的对象(如果提供,新对象引用计数+1)或重置为空指针。可指定自定义deleter
use_count()获取当前接管对象的引用计数。空指针调用返回为0
swap()交换接管的对象,原对象的引用计数不变

weak_ptr

std::weak_ptr不能直接构造,必须从一个shared_ptr或另一个weak_ptr构造。它的构造和析构不会引起引用计数的增加或减少。

API描述
lock()返回接管对象的shared_ptr(引用计数+1),如果接管对象已被释放,则返回空的shared_ptr
reset()重置为空指针,注意引用计数不变
use_count()获取当前接管对象的shared_ptr指针的引用计数。
swap()交换接管的对象,原对象的引用计数不变
expired()判断当前weak_ptr是否还有托管的对象,有则返回false,无则返回true
  • 不支持 *-> 对指针的访问

Example:

c++
#include <iostream>
#include <memory>

using namespace std;

class Person
{
private:
	std::string _name;
public:
	Person() { 
		std::cout << "调用Person的构造函数" << std::endl;
		_name = "Unknown";
	}
	Person(string name) {
		std::cout << "调用Person的带参构造函数: " << name << std::endl; 
		_name = name;
	}
	~Person() { std::cout << "调用Person的析构函数" << std::endl; }
	void doSomething() const { std::cout << _name << " doSomething...\n"; }
};

class PersonDeleter
{
public:
	void operator()(Person* pt) {
		pt->doSomething();
		delete pt;
	}
};

class Girl;

class Boy
{
private:
    weak_ptr<Girl> _girlFriend;
public:
    Boy() 
    {
        cout << "Boy构造函数\n";
    }
    virtual ~Boy()
    {
        cout << "~Boy析构函数\n";
    }
    void setGirlFriend(shared_ptr<Girl> girlFriend)
    {
        cout << "In setGirlFriend a: " << girlFriend.use_count() << endl; // 2
        // 赋值给weak_ptr不会让引用计数+1
        // 这里是因为girlFriend作为值传递方式传参而使计数+1
        // 当函数执行完之后,girlFriend会销毁使计数-1
        _girlFriend = girlFriend;
        cout << "In setGirlFriend b: " << _girlFriend.use_count() << endl; // 2
        // 获取共享指针
        shared_ptr<Girl> spGril =_girlFriend.lock();
        cout << "In setGirlFriend c: " << spGril.use_count() << endl; // 3
        spGril = nullptr;
        cout << "In setGirlFriend d: " << _girlFriend.use_count() << endl; // 2
    }
    void resetGirlFriend()
    {
        if (!_girlFriend.expired())
        {
            _girlFriend.reset();
            cout << "分手成功!\n";
        }
        else
            cout << "已经没有女朋友了!\n";
    }
};

class Girl {
private:
    shared_ptr<Boy> _boyFriend;
public:
    Girl() {
        cout << "Girl构造函数" << endl;
    }

    virtual ~Girl() {
        cout << "~Girl析构函数" << endl;
    }

    void setBoyFriend(shared_ptr<Boy> boyFriend) {
        _boyFriend = boyFriend;
    }
};
c++
#include "smart_ptr.hpp"

void test_unique_ptr()
{
  std::cout << "===================Test unique_ptr==========================\n";
  std::unique_ptr<string> ptr_string(new string("zhangsan"));
  std::unique_ptr<int> ptr_int(new int(100));
  {
    Person* person = new Person();
    std::unique_ptr<Person> p2;
    p2.reset(person);  // 接管person实例的内存空间
    std::unique_ptr<Person> p3;
    p3 = std::move(p2);  // 使用move把左值变为右值后就可以赋值了,p2被置为空
    p3.reset();  // 等同于 p3 = nullptr;
    // auto p = p3.release(); // 仅释放托管对象的控制权,不销毁对象。返回对象指针
    // delete p; // 如果使用release(),则需要手动释放内存
  }
  std::cout << "\n*****测试自定义的 deleter ***** \n";
  {
    Person* person2 = new Person("Tom");
    std::unique_ptr<Person, PersonDeleter> p4(person2);
  }
}
c++
#include "smart_ptr.hpp"

void test_shared_ptr()
{
  std::cout << "===================Test shared_ptr==========================\n";
  std::shared_ptr<int> ptr_int = make_shared<int>(100);
  {
    std::shared_ptr<Person> p1 = make_shared<Person>("Tom");
    std::shared_ptr<Person> p2;
    std::cout << "p2.use_count() = " << p2.use_count() << std::endl;
    p2 = p1;
    std::cout << "count: " << p1.use_count() << " " << p2.use_count() << std::endl;
    // 2 2
    p2.reset();  // 当前接管对象的引用计数-1,并将p2置为空指针。等同于 p2 = nullptr;
    std::cout << "count: " << p1.use_count() << " " << p2.use_count() << std::endl;
    // 1 0
  }
  {
    Person* person = new Person("Lucy");
    std::shared_ptr<Person> p3(person, PersonDeleter());
    std::shared_ptr<Person> p4(p3); // 等同于 p4 = p3;
    std::cout << "count: " << p3.use_count() << " " << p4.use_count() << std::endl;
    p4 = nullptr; // p4没有使用自定义deleter
    std::cout << "count: " << p3.use_count() << " " << p4.use_count() << std::endl;
  }
  {
    Person* person = new Person("Lucy2");
    // 用下面的方式初始化p5,在运行p5.reset时会异常退出
    //std::shared_ptr<Person> p5(nullptr, PersonDeleter());
    std::shared_ptr<Person> p5;
    p5.reset(person, PersonDeleter());
    std::cout << "p5 count: " << p5.use_count() << std::endl;
    p5.reset();  // 使用自定义deleter
    std::cout << "p5 count: " << p5.use_count() << std::endl;
  }
}
c++
#include "smart_ptr.hpp"

void test_weak_ptr()
{
  cout << "===================Test weak_ptr==========================\n";
  shared_ptr<Boy> spBoy(new Boy());
  shared_ptr<Girl> spGirl(new Girl());
  
  spGirl->setBoyFriend(spBoy);

  cout << "Before setGirlFriend: " << spGirl.use_count() << endl; // 1
  spBoy->setGirlFriend(spGirl);
  cout << "After setGirlFriend: " << spGirl.use_count() << endl; // 1
  spBoy->resetGirlFriend();
  cout << spGirl.use_count() << endl;  // 1
}

编程指南

  1. 如果程序需要使用多个指向同一个对象的指针,应该使用shared_ptr;如果有循环引用的情况,则使用weak_ptr
  2. unique_ptr可以转化为shared_ptr,但反之则不行。
  3. 不要把原生对象的指针托管给多个智能指针,这非常危险!
    c++
    int* x = new int(10);
    shared_ptr<int> p1(x);  // 或使用p1.reset(x)的形式;
    shared_ptr<int> p2(x);
    p1 = nullptr;  // p1所接管的指针所指向的内存空间会被释放。
    // p2 所接管的指针将变成悬挂指针。
  4. 如果使用了release(),记得手动释放返回的指针。
  5. 不要delete智能指针get()返回的指针。既然用上了智能指针,那就让代码“智能”一点。
  6. 智能指针作为函数参数或返回值的情况
    函数unique_ptrshared_ptr
    返回值所有权转让给接收返回值的unique_ptr复制行为,引用计数+1,但函数执行完后会自动销毁,引用计数-1
    参数:值传递所有权转让给函数的局部变量参数,函数执行完后会销毁(谨慎)同上
    参数:引用传递所有权仍归函数外的实参,函数执行完后不会销毁计数不变
    c++
     static unique_ptr<Person> func1(string name)
     {
         return unique_ptr<Person>(new Person(name));
     }
    
     static void func2(unique_ptr<Person> up)
     {
         up->doSomething();
     }
    
     static void func3(unique_ptr<Person>& up)
     {
         up->doSomething();
     }
    
     void test_func_unique_ptr()
     {
       auto p5 = func1("P5");
       func3(p5);
    
       Person* person3 = new Person("P6");
       unique_ptr<Person> p6(person3);
       func2(std::move(p6)); // 函数执行完后,person3的内存会被释放
       cout << "After func2\n";
     }
    运行结果如下:

Last updated:

本站访客数 人次 本站总访问量