Skip to content

C++

更新: 5/12/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++
class Stack
{
public:
  enum { MAX_SIZE = 100 };
  // static constexpr int MAX_SIZE = 100;
  int arr[MAX_SIZE];
}

结构体

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

定义常量

  1. const修饰的变量不可修改,且必须初始化。
  2. const修饰的常量可以是编译时常量,也可以是运行时常量,具体取决于其初始化方式。
  3. 非const变量默认是extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。
c++
// 不指定extern,则只能在当前文件内访问
extern const int BUF_SIZE = 100;  // 编译时常量

int getSize()
{
  return sizeof(int);
}

int main() {
  const int n = 10;  // 编译时常量
  cosnt int n1 = 5 * n; // 常量表达式也是编译时常量
  int arr[n]{};
  const int size = getSize();  // 运行时常量:需要运行时才能确定
}

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与函数

const与类

  1. const修饰成员变量,必须使用初始化列表初始化,不能在构造函数内初始化。
  2. const修饰成员函数,不能修改成员变量的值,且只能调用const成员函数。
  3. const修饰类的实例对象,该对象的所有成员变量都不能修改。
c++
class Person {
public:
  const int age_ = 0;
  Person(int age) : age_(age) {
    // this->age_ = age;
  }
  void display() const;
}

const Person p1(10);
// p1.age = 20;

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;
    }

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
}

typedef

用于定义类型的别名,便于代码的可读性和维护性。

c++
typedef unsigned int Item;  // 定义unsigned int类型的别名为Item

Item x = 10;

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

explicit

  • 当类的构造函数只有一个输入参数时(其它参数有默认值也算只有一个输入),此构造函数可隐式将参数值转换为类对象。explicit修饰此构造函数后,将关闭此特性,但仍可以显式强制类型转换。
  • 修饰转换函数时,可以防止隐式转换,但按语境(通常是在与布尔相关的条件判断或布尔逻辑运算符中的语境)转换除外。
c++
class Person
{
public:
  // 构造函数 
  explicit Person(std::string name, int age=0): name_(name) {}
  // 转换函数1:将Person对象转换为string类型
  explicit operator std::string() const { return name_; }
  // 转换函数2:将Person对象转换为int类型
  operator int() const { return age_; }
  explicit operator bool() const { return !name_.empty(); }
private:
  std::string name_;
  int age_ = 0;
};

int main() {
  Person p1 = Person("Tom");
  // Person p1 = "Tom";  // 隐式转换,构造函数使用explicit后会报错
  Person p2 = (Person) "Tom";  // 显式强制转换

  std::string name = (std::string) p2;  // 强制类型转换
  int age = p2;  // 隐式调用转换函数2,转换函数使用explicit后会报错

  if (p2) {}
}

提示

谨慎使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的函数。

函数

函数参数

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

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

函数返回对象的情况

返回对象

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

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

返回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类没有公有的复制构造函数。

函数重载

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

函数模板

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

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

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. 初始化列表
  2. 构造函数体内赋值
  3. 使用默认参数
  4. 委托构造
c++
class Person 
{
public:
    const int id_;  // const成员变量只能使用初始化列表的方式
    string name_;
    short age_;
    double height_;

    Person(int id, const string& name, short age)
      : id_(id), name_(name) {  // 1. 初始化列表
      age_ = age;
      height_ = 0.0;  // 2. 构造函数体内赋值
    }
    Person(int id, const string& name, double height, short age=0)//3. 使用默认参数
      : Person(id, name, age) {  // 4. 委托构造
      height_ = height;
    }
};

析构函数

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

复制构造函数

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

c++
ClassName(const ClassName &);  // 声明

ClassName::ClassName(const ClassName & cls) {}  // 定义

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

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

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

赋值运算符

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

c++
ClassName& operator=(const ClassName &); // 声明

ClassName& ClassName::operator=(const ClassName & cls)  // 定义
{
  if (this == &cls)
  {
    return *this;
  }
  ...
  return *this;
}

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

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

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

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

封装

访问修饰符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成员。

基类方法

派生类不会继承基类的哪些方法:

  • 构造函数:派生类需要定义自己的构造函数。创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。派生类通常使用成员初始化列表来调用基类构造函数。
  • 析构函数:释放对象时,程序首先调用派生类的析构函数,然后调用基类的析构函数。基类的析构函数应当是虚的
  • 赋值运算符:重载函数的特征标不一样。
  • 友元函数:友元函数不属于类成员,因此不能被继承。友元函数是全局的,所以派生类也能直接调用友元函数,但最好是将派生类(指针或引用)强制转换为基类来调用基类的友元函数。

派生类访问基类:

  • 访问基类方法:使用作用域解析运算符::
  • 获取基类对象本身:
    cpp
    const Person & Student::getPerson() const
    {
      return (const Person &) *this;
    }
  • 访问基类友元函数:
    c++
    ostream & operator<<(ostream & os, const Student & s)
    {
      os << (const Person &) s << " " << s.teacher << endl;
      return os;
    }
  • 对于保护继承和私有继承,基类的公有方法在派生类中属于保护成员和私有成员。如何让基类的公有方法在派生类外也能访问呢?
    1. 在派生类定义一个公有方法调用基类的公有方法。
    2. 在public中使用using声明。
      c++
      class Student : private Person
      {
      public:
         // 注意声明只使用成员名——没有圆括号、函数特征标和返回类型。
         using Person::show;  
      }

提示

基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象,但只能调用基类方法

多重继承

多重继承带来的问题:SingingWaiter实例对象的Singer对象和Waiter对象都有Worker对象,但实际只需要一个Worker对象。由此C++引入虚基类。

虚基类

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。

c++
class Singer : public virtual Worker {...};
class Waiter : virtual public Worker {...};

class SingingWaiter : public Singer, public Waiter {...};
新的构造函数规则

假设有以下构造函数:

c++
SingingWaiter(const Worker& wk, int s=0, int w=0) 
  : Singer(wk, s), Waiter(wk, w) {}

为避免当wk能通过两条途径传递给Worker对象,C++在基类是虚基类时,禁止信息通过中间类自动传递给基类。上述构造函数将初始化成员s和w,但wk参数中的信息不会传递给子对象Worker,而是使用Worker类的默认构造函数。

如果不希望使用虚基类的默认构造函数,则需显式调用虚基类的构造函数:

c++
SingingWaiter(const Worker& wk, int s=0, int w=0) 
  : Worker(wk), Singer(wk, s), Waiter(wk, w)  {}
同名函数

在上面的类图中,假设在SingingWaiter中没有定义show方法,那将调用哪个父类的show方法呢?这就会产生歧义性。如何解决?

  1. 在SingingWaiter类中重新定义show方法。(推荐)
  2. 使用作用域解析运算符:singingWaiter.Singer::show();这将调用Singer类的show方法。

多态

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

实现方式:

  • 重新定义:在派生类中重新定义基类的方法,通过不同的派生类调用方法。
  • 虚函数:
    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);
}

虚函数

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

抽象类

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

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

友元

友元类

  • 我的朋友由我自己声明
  • 友元类的所有方法都可以访问原始类的私有成员和保护成员。
  • 友元声明可以位于公有、私有或保护部分,其所在位置无关紧要。
c++
class Tv
{
private:
  int state;
  int channel;
public:
  enum { OFF, ON };
  // 声明Remote类是友元类
  friend class Remote;

  Tv(int s = OFF) : state(s) {}
  void onOff() { state = (state == ON) ? OFF : ON; }
}

class Remote
{
public:
  void onOff(Tv &t) { t.onOff(); }
  void setChannel(Tv &t, int c) { t.channel = c; }
}

友元成员函数

可选择仅让特定的类成员称为另一个类的友元,而不必让整个类称为友元,但这样做要非常小心各种声明和定义的顺序,要使用前向声明。上面的代码中只有setChannel方法直接访问了Tv类的成员,因此可以让此方法成为友元成员函数。

c++
class Tv;  // 前向声明

class Remote
{
public:
  void onOff(Tv &t);  // 只能声明,因为此时还不清楚Tv类的成员方法
  void setChannel(Tv &t, int c); 
};

class Tv
{
private:
  int state;
  int channel;
public:
  friend void Remote::setChannel(Tv &t, int c);  // 声明友元的成员函数

  Tv(int s = OFF);
  void onOff();
}

// 此时编译器已经知道Tv类的成员,可以定义(或在实现文件中定义)
inline void Remote::setChannel(Tv &t, int c){t.channel = c;}
c++
#include "tv.hpp"

Tv::Tv(int s) : state(s) {}
void Tv::onOff() { state = (state == ON) ? OFF : ON; }

void Remote::onOff(Tv &t) { t.onOff(); }

可以相互声明自己是对方的友元类:

c++
class Tv
{
friend class Remote;
public:
  void buzz(Remote &r);  // 只能声明,因为此时编译器没有足够的信息
}

class Remote
{
friend class Tv;
public:
  void onOff(Tv &t) { t.onOff(); }  // 可以定义
}

inline void Tv::buzz(Remote &r) {  }

友元重载运算符

很多运算符可以使用成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应该是友元函数

c++
Time operator+(const int hour)
{
  Time res = *this;
  res.hour = res.hour + hour;
  return res;
}
c++
friend Time operator+(const int hour, const Time & t);  // 声明友元函数
// 定义友元函数,注意不能带friend
Time operator+(const int hour, const Time & t)
{
  Time res = t;
  res.hour = t.hour + hour;
  return res;
}
  • 对于成员函数,语句 t2 = t1 + 3 将转换为 t2 = t1.operator+(3)
  • 对于友元函数,语句 t2 = 3 + t1 将转换为 t2 = operator+(3, t1)。 对成员函数来说,一个操作数通过this指针隐式传递,另一个操作数作为函数参数显式传递;而对于友元函数来说,两个操作数都作为参数显式传递。

提示

如果将非类的项作为重载运算符函数的第一个操作数,应该使用友元函数。

重载<<运算符时需要注意返回ostream &,以便能继续输出后续的内容:

c++
friend ostream & operator<<(ostream & os, const Time & t);

ostream & operator<<(ostream & os, const Time & t)
{
  os << t.hour << ": " << t.minute << ": " << t.second;
  return os;
}

Time t1(3, 4, 5);
cout << t1 << " PM\n";
// cout << t1 返回的是 ostream &,所以能继续 cout << " PM"

类模板

模板形式

c++
// 定义模板
template <typename T>
class ClassName {
private:
  T m_data;
public:
  ClassName(T);
}

// 实现模板
template <typename T>
ClassName<T>::ClassName(T t) 
{
  m_data = t;
}

// 使用模板类
ClassName<int> cn(1);
c++
template <typename T, int n>
class ClassName2 {}
c++
// 参数可带默认类型
template <typename T1, typename T2 = int>
class ClassName3 {}

模板可以作为基类,也可用作组件类,还可用作其他模板的类型参数。

提示

通常将模板的声明和定义放到同一个头文件中。

具体化

同函数模板,类模板的具体化也分为隐式实例化、显式实例化和显式具体化。

c++
// 编译器在需要对象之前,不会生成类的隐式实例化
ClassName2<int, 10> * ptr;  // 指针,还不需要对象,此步骤不会生成类
ptr = new ClassName2<int, 10>;  // 编译器将生成类定义,并根据该定义创建对象
c++
// 虽没有创建或提及类对象,编译器也将生成类声明(包括方法定义)
template class ClassName2<string, 10>;
c++
// 显式具体化的类模板定义,对特定类型,类模板有不同的行为(与函数模板用意相同)。
template <> class ClassName<char*> {};
c++
// 假设有一个通用的类模板定义
template <typename T1, typename T2> class TestClass {};

// 指定部分类型参数的类型,如指定T2为int类型
template <typename T1> class TestClass<T1, int> {};

成员模板

模板可以作为结构体、类或模板类的成员。

c++
template <typename T>
class Beta
{
private:
    template <typename V>  // 嵌套模板作为模板类的成员
    class Hold
    {
    private:
        V val;
    public:
        Hold(V v=0): val(v) {}
        void show() const { cout << val << endl; }
        V value() const { return val; }
    };

    Hold<T> q;
    Hold<int> n;
public:
    Beta(T t, int i): q(t), n(i) {}

    template<typename U>  // 模板方法
    U blab(U u, T t) { return (q.value() + n.value()) * u / t; }

    void show() const { q.show(); n.show(); }
};
c++
template<typename T>
class Beta
{
private:
    template <typename V>
    class Hold;
    Hold<T> q;
    Hold<int> n;
public:
    Beta(T t, int i) : q(t), n(i) {}

    template<typename U>
    U blab(U u, T t);

    void show() const { q.show(); n.show(); }
};

template <typename T>
template <typename V>
class Beta<T>::Hold
{
private:
    V val;
public:
    Hold(V v = 0) : val(v) {}
    void show() const { cout << val << endl; }
    V value() const { return val; }
};

template <typename T>
template <typename U>
U Beta<T>::blab(U u, T t)
{
    return (n.value() + q.value()) * u / t;
}

模板作为参数

c++
template <template <typename T> typename U, typename V>
class Crab {
private:
    U<int> s1;
}
// ...
Crab<ClassName, int> c1;

U作为模板的参数,其本身也是一个类模板,类U模板的的声明是template <typename T>,实例对象c1中的ClassName模板类要符合此声明。U<int>将被替换成ClassName<int>

模板类和友元

  • 非模板友元:在模板类中将一个常规函数声明为友元。
  • 约束模板友元:友元的类型取决于类被实例化时的类型,即类模板的参数类型作为友元函数模板参数类型的一部分。
  • 非约束模板友元:友元的所有具体化都是类的每一个具体化的友元,即类模板的参数类型和友元函数模板的参数类型无关。
c++
template <typename T>
class HasFriend
{
public:
    HasFriend(const T& t) : item(t) { ct++; }
    ~HasFriend() { ct--; }
    friend void counts();
    friend void reports(HasFriend<T>&);
private:
    T item;
    static int ct;
};


template <typename T>
int HasFriend<T>::ct = 0;
void counts()
{
    cout << "int count: " << HasFriend<int>::ct << ";";
    cout << "double count: " << HasFriend<double>::ct << endl;
}
void reports(HasFriend<int>& hf)
{
    cout << "HasFriend<int>: " << hf.item << endl;
}
void reports(HasFriend<double>& hf)
{
    cout << "HasFriend<double>: " << hf.item << endl;
}

int main()
{
    cout << "No Objects declared: ";
    counts();
    HasFriend<int> hfi1(10);
    cout << "After hfi1 declared: ";
    counts();
    HasFriend<int> hfi2(20);
    cout << "After hfi2 declared: ";
    counts();
    HasFriend<double> hfdb(10.5);
    cout << "After hfdb declared: ";
    counts();

    reports(hfi1);
    reports(hfi2);
    reports(hfdb);
}
c++
template <typename T> void counts();
template <typename T> void reports(T&);

template <typename TT>
class HasFriend
{
public:
    HasFriend(const TT& t) : item(t) { ct++; }
    ~HasFriend() { ct--; }
    friend void counts<TT>();
    friend void reports<HasFriend<TT>>(HasFriend<TT>&);
private:
    TT item;
    static int ct;
};


template <typename T>
int HasFriend<T>::ct = 0;

template <typename T>
void counts()
{
    cout << "template size: " << sizeof(HasFriend<T>) << endl;
    cout << "template counts: " << HasFriend<T>::ct << endl;
}

template <typename T>
void reports(T& hf)
{
    cout << hf.item << endl;
}

int main()
{
    counts<int>();
    HasFriend<int> hfi1(10);
    HasFriend<int> hfi2(20);
    HasFriend<double> hfdb(20.5);
    reports(hfi1);
    reports(hfi2);
    reports(hfdb);
    cout << "counts<int>() output: ";
    counts<int>();
    cout << "counts<double>() output: ";
    counts<double>();
}
c++
template <typename T>
class ManyFriend
{
private:
    T item;
public:
    ManyFriend(const T& t): item(t) {}
    template <typename C, typename D> friend void show(C&, D&);
};


template <typename C, typename D> void show(C& c, D& d)
{
    cout << c.item << ", " << d.item << endl;
}

int main()
{
    ManyFriend<int> hfi1(10);
    ManyFriend<int> hfi2(20);
    ManyFriend<double> hfdb(20.5);
    show(hfi1, hfi2);
    show(hfdb, hfi2);
}

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";
     }
    运行结果如下:
本站访客数 人次 本站总访问量