C++ 侯捷课程知识整理

前言

本文整理C++基础知识,用于开发中日常查阅。

大气的c++程序

  • 类构造函数使用列表初始化,提高效率
  • 创建类对象时尽量使用列表初始化直接创建,提高效率
  • 添加类成员函数时候时刻注意是否是常量成员函数,即添加const
  • 参数考虑是不是pass by reference,或者pass by value
  • 考虑函数返回值是return by reference,或者return by value
  • 头文件内部可以实现短小精悍的方法,即实现内联函数
  • 短小的方法考虑添加inline关键字做内联
  • 任何一种函数都可以在成员函数里还是全局函数里实现,操作符重载函数也是如此

static关键字

  • 静态成员变量
    - 所有对象共享同一份数据
    - 在编译阶段分配内存
    - 静态成员变量需要在类外进行初始化
  • 静态成员函数
    - 所有对象共享同一个函数
    - 静态成员函数只能访问静态成员变量
struct A {
    
    
    A() {
    
    
        cout << "A 构造" << endl;
    }
};

// 头文件内
class Account {
    
    
public:
	// 静态成员变量必须在类外初始化,声明无定义,此处仅仅是声明了,没有实际分配内存
    static double m_rate; 
    static A a; 
    
    static void set_rate(const double x) {
    
    
        m_rate = x;
    }
};

// cpp内,使用前必须进行定义
double Account::m_rate = 0.0; // 静态成员需在类外进行初始化
A Account::a; // 定义

int main() {
    
    
    Account::set_rate(5.0);
    cout << Account::m_rate << endl;
    cout << Account::a.a << endl;
	return 0;
}

引用,漂亮的指针

引用即别名,引用并非对象,它是为一个已经存在的对象所起的另一个名字。

  • 引用必须初始化
  • 引用在初始化后,不可以改变
  • 引用类型必须和绑定的对象的类型一致,但常量引用会丢失精度
  • 引用类型的初始值必须是一个变量,常量引用可以引用常量
int a = 1;
int &ra = a; // 引用必须要初始化

int c = 2;
ra = c; // 赋值操作,把c赋值给b,即赋值给a
// &rb = c; 引用在初始化后,不可以改变

// Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
const int e = 10;
// int &re = e; // 引用类型的初始值必须是一个对象
const int &re = e;

double d = 10.0F;
// Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
// int &rd = d; // 引用类型必须和绑定的对象的类型一致,添加const后可以
double &rd = d; 
const int &rd1 = d; // 正确,Clang-Tidy: Narrowing conversion from 'double' to 'int'

引用做函数参数

函数传参时,可以利用引用的技术,让形参修饰实参,可以简化指针修改实参。

引用做函数返回值

引用是可以作为函数的返回值存在的。函数调用作为左值。
注意:不要返回局部变量引用

//返回局部变量引用
int& test01() {
    
    
    int a = 10; //局部变量,内存分配在栈内存上,方法执行完后会回收这块内存
    return a;
}

//返回静态变量引用
int& test02() {
    
    
    static int a = 20;
    return a;
}

int main() {
    
    
    //不能返回局部变量的引用
    int& ref = test01();
    cout << "ref = " << ref << endl;
    cout << "ref = " << ref << endl;

    //如果函数做左值,那么必须返回引用
    int& ref2 = test02();
    
    // 函数调用可以作为左值
    test02() = 1000;

    return 0;
}

引用的本质

引用的本质在c++内部实现是一个指针常量,是所有的指针操作编译器都帮我们做了。

//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
    
    
    ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
    
    
    int a = 10;

    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
    int& ref = a; 
    ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;


    func(a);
    return 0;
}

const关键字

默认情况下,const对象仅在文件内有效。

  • 编译器在编译过程中会把用到该const变量都替换成对应的值。
    添加volatile后,可以禁止编译器对该值进行优化,必须在运行时去读取该const值。

  • 默认情况下,const对象仅在文件内有效。
    当多个文件中出现同名const变量时,其实等同于在不同文件中分别定义了独立的变量。因为在编译时会替换,所以相当于是独立的变量。

  • 可以使用extern关键字修饰,让他可以在多个文件中被共享。
    在声明和定义中都添加extern关键字,这样就只需要定义一次,别的文件也能使用该常量了。

// file1.h头文件中声明,用extern表明该const值并非本文件独有,他的定义将在别处出现
extern const int buffer_size;
// file1.cc文件中定义
extern const int buffer_size = 512;

const修饰变量

  • const修饰的变量也称为常量。
    使用关键字const对限定变量的值,避免其他操作改变该值,不可修改;
    const 数据类型 变量名 = 常量值

  • const对象一旦创建后就不能改变,所以必须对其进行初始化。

const修饰表达式

常量表达式constexpr, const expression,是指值不会改变并且在编译过程就能得到计算结果的表达式。

const int max_file = 20;  // 常量表达式
const int limit = max_file + 1; // 常量表达式
const int size = get_size(); // 不是常量表达式,得运行时才能确定


constexpr int max= 20;  // 常量表达式
constexpr int limit = max+ 1; // 常量表达式
constexpr int sz = size(); // 只有当size()函数是一个constexpr函数时才是正确的

const修饰指针

也可以用指针指向const常量或非常量。
指向常量的指针不能用于改变其所指对象的值,也只能使用指向常量的指针。

区分指针常量和常量指针,可以从右往左读,看const具体修饰的是谁。

  • 底层const,表示指针所指的对象是一个常量
    常量指针,const int *p = &PI。指针指向的值不可更改,但指针的指向可以更改。
    常量指针必须初始化,一旦初始化不可更改。

  • 顶层const,表示指针本身是个常量
    指针常量,int *const p2 = &a。指针的指向不可以改,指针指向的值可以改,

  • 当对象执行拷贝操作时,进行拷贝的对象要具有相同的底层const资格,或者这两个对象必须的数据类型必须能够互相转换。

const double PI = 3.14;
const double *p = &PI; // 正确,必须添加const关键字,即不能通过*p赋值PI,常量指针

int a = 1;
int b = 2;

// 常量指针,指针指向的值不可以改,但指针的指向可以改
const int *p = &a;
// *p = b; 错误,指向的值不可以改
p = &a; // 指针的指向可以改

// 指针常量,指针的指向不可以改,指针指向的值可以改,
int *const p2 = &a;
*p2 = b; // 指针指向的值可以改
// p2 = &b; 错误,指针的指向不可以改

// const修饰指针和常量,都不可以改
const int *const p3 = a;
// *p3 = b;
// p3 = &b;

const修饰引用

可以把引用绑定到const对象上,就像绑定到其它对象上一样,称之为对常量的引用,可以成为常量引用。
当然const也可以修饰引用,即引用非常量。

int a = 1024;
const int &ra = a;  // 正确,允许将一个常量引用绑定到普通对象上,但不允许通过ra改变a的值,a的值依然可以变化

const int b = 1024;
const int &rb = b;   // 正确,引用及其对应的常量都是常量

ra = 1;// 错误,不能修改常量
rb= 1;  // 错误,不能修改常量
int &r2 = b;  // 错误,必须是一个常量引用才行

double c = 3.14;
int &rc = c;  // 错误,类型不匹配,Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
const int &rc1 = c; // 正确,Clang-Tidy: Narrowing conversion from 'double' to 'int'

const修饰函数形参

可以使用const修饰函数入参,防止在函数体内部修改该传递过来的变量

class Person {
    
    
public:
    string name;
    mutable int age;
};

void testPerson(const Person *person) {
    
    
    person->age = 1; // 可以修改
    // person->name = "aa"; // 不可修改
}

void testPerson(const Person& person) {
    
    
    person.age = 1; // 可以修改
    // person.name = "aa"; // 不可修改
}

const修饰成员函数

使用const修饰的成员函数,即常量成员函数,const成员函数内部不允许修改非mutable成员变量。

class Person {
    
    
private:
    int age; // 常量成员函数内不允许修改非mutable的值
    mutable int number; // 常量成员函数内可以修改mutable的值
public:
    int getAge() const {
    
    
        // age = 1; // 错误,不能更改
        return age;
    }

    int getNumber() const {
    
    
        number = 1;
        return number;
    }
};

常量成员函数重载规则

  • 如ClassA所示:在设计函数方法时需要考虑是否设计成常成员函数,因为常成员函数可以被常量对象调用,也可以被非常量对象调用;
  • 如ClassB所示:当常成员函数和普调成员函数共存时,则常量对象调用常成员函数,非常量对象调用普调成员函数;
class ClassA {
    
    
public:
    ClassA() {
    
    }
    void print() const {
    
     // 添加const
    }
};

class ClassB {
    
    
public:
    void print() {
    
    
    }
    // 函数重载,const也是函数签名的一部分
    void print() const {
    
    
    }
};

int main() {
    
    
    const ClassA a;
    // 'this' argument to member function 'print' has type 'const Test', but function is not marked const
    a.print();
	
	const ClassB b1;
    b1.print();
    
    ClassB b2;
    b2.print();
    
    return 0;
}

在这里插入图片描述

操作符重载

在这里插入图片描述

  • 使用operator关键字,专门用于定义重载运算符的函数,即operator+函数
  • 运算符重载不改变原运算符优先级
  • 等价于函数调用,即a + b 等价于函数调用a.operator+(b)
  • 运算符重载函数也可以是全局函数

自定义complex复数类

均在头文件内实现,所有函数需添加inline关键字。

#ifndef TESTCPP_TESTOPERATOR_TEST_H
#define TESTCPP_TESTOPERATOR_TEST_H

#include <iostream>
using namespace std;

class complex {
    
    
public:
    explicit complex(double real = 0, double imag = 0) : re(real), im(imag) {
    
    }

    // 成员函数重载
    complex &operator+=(const complex &x);

    // 友元函数,可以访问private值
    friend complex operator+(const complex &x, const complex &y);

    inline double real() const {
    
    
        return re;
    }

    inline double imag() const {
    
    
        return im;
    }

    inline void display() const {
    
    
        cout << re << "+" << im << "i" << endl;
    }

private:
    double re;
    double im;
};


inline complex &
complex::operator+=(const complex &x) {
    
    
    this->re += x.re;
    this->im += x.im;
    return *this;
}

// 任何一种函数都可以在成员函数里还是全局函数里实现
inline double real(const complex &x) {
    
    
    return x.real();
}

inline double imag(const complex &x) {
    
    
    return x.imag();
}

// 共轭复数
inline complex conj(const complex &x) {
    
    
    return complex(real(x), -imag(x));
}

/*
 * 在全局范围内重载
 * 在操作符重载时考虑成员函数内重载 or 全局函数重载
 * 如下加法重载,放在全局函数
 */

// 加法,两个参数
inline complex operator+(const complex &x, const complex &y) {
    
    
	//    尽量使用列表初始化
    //    complex result;
    //    result.re = x.re + y.re;
    //    result.im = x.im + y.im;
    //    return result;

    return complex(real(x) + real(y), imag(x) + imag(y));
}

inline complex operator+(const complex &x, double y) {
    
    
    return complex(real(x) + y, imag(x));
}

inline complex operator+(double x, const complex &y) {
    
    
    return complex(x + real(y), imag(y));
}

// 正号
inline complex operator+(const complex &x) {
    
    
    return x;
}

// 负数
inline complex operator-(const complex &x) {
    
    
    return complex(-real(x), -imag(x));
}

// 相等
inline bool operator==(const complex &x, const complex &y) {
    
    
    return real(x) == real(y) && imag(x) == imag(y);
}

// 特殊的操作符<< 只能全局函数重载
inline ostream &operator<<(ostream &out, const complex &x) {
    
    
    return out << x.real() << "+" << x.imag() << "i";
}

#endif //TESTCPP_TESTOPERATOR_TEST_H

模板

模板类

类模板使用时候需要明确指示类型。

template<typename T>
class complex {
    
    
public:
    complex(T r, T i) : re(r), im(i) {
    
    
    }
    
    T real() const {
    
     return re; }
    T imag() const {
    
     return im; }
private:
    T re, im;
};

int main {
    
    
	complex<double> a(1.5, 1.5);
    complex<int> b(1, 1);
	return 0;
}

模板方法

如下模板方法,任意类型都可以,只要重载了<符号。

template<class T>
inline const T& min(const T& a, const T& b) {
    
    
    return a < b ? a : b;
}

多态与virtual

  • 多态分为两类
    - 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
    - 动态多态: 派生类和虚函数实现运行时多态

  • 静态多态和动态多态区别:
    - 静态多态的函数地址早绑定 - 编译阶段确定函数地址
    - 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

  • 动态绑定的三个条件
    - 通过指针调用
    - 调用的方法是一个虚函数
    - 调用者向上转型,即通过父类调用

  • 虚析构和纯虚析构
    - 虚析构或纯虚析构是用来解决通过父类指针释放子类对象
    - 如果子类中没有堆区数据,可以不添加虚析构和纯虚析构
    - 纯虚析构需要在类外进行实现
    - 拥有纯虚析构的函数也是一个抽象类,不能被实例化

何时添加virtual关键字

在设计类时添加virtual关键字:

  • non-virtual函数:不希望派生类重新定义它,即不希望子类overrid它
  • virtual函数:希望派生类重新定义它,但是有一个默认的实现
  • pure virtual函数: 派生类一定要重新定义它才能被实例化,含义纯虚函数的类是一个抽象类,不能被实例化

虚指针vptr和虚表vtbl

观察者模式

struct Observer {
    
    
public:
    virtual void onNotify() = 0;
};

class Subject {
    
    
public:
    void addObserver(Observer *observer) {
    
    
        m_views.push_back(observer);
    }

    void notify() const {
    
    
        for (auto obj : m_views) {
    
    
            obj->onNotify();
        }
    }

private:
    vector<Observer *> m_views;
};

// public公有继承
class MyObserver : public Observer {
    
    
	void onNotify() override {
    
    
	    cout << "MyObserver onNotify" << endl;
	}
};

void main() {
    
    
    Subject subject;
    MyObserver observer;

    subject.addObserver(&observer);
    subject.notify();

    return 0;
}

Big Three: 拷贝构造、拷贝赋值和析构

  • 默认情况下,c++编译器至少给一个类添加4个函数
    - 缺省的无参构造函数,A() = default;
    - 缺省的拷贝构造函数,对属性进行值拷贝,按位拷贝,A(const A& a) = default;
    - 缺省的拷贝赋值函数,对属性进行值拷贝,按位拷贝,A& operator=(A& a) = default;
    - 缺省的析构函数 ~A() = default;

  • C++中拷贝构造函数调用时机通常有三种情况
    - 使用一个已经创建完毕的对象来初始化一个新对象
    - 值传递的方式给函数参数传值
    - 以值方式返回局部对象

  • 重写拷贝赋值函数时需要考虑自拷贝这种情况

深拷贝、浅拷贝

编译期默认的是浅拷贝,按位拷贝,所以如果成员变量为指针类型的话,会导致两个对象指向同一块内存;
自定义class如果内部有成员变量为指针类型的话,则必须重写拷贝构造、拷贝赋值和析构函数,即进行深拷贝。

当要进行深拷贝时,即类成员有指针时,如果不进行深拷贝,就会有双重free问题(pointer being freed was not allocated),所以必须进行深拷贝;
而进行移动时候会将原先的指针置为NULL,所以不会有这种问题,即是浅移动。

左值、右值与右值引用

  • 左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
    变量、函数或数据成员的名字
    返回左值引用的表达式,如 ++x、x = 1、cout << ’ ’
    字符串字面量如 “hello world”
    在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。

  • 纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为临时对象,即临时对象就是一种右值,最常见的情况有:
    返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
    除字符串字面量之外的字面量,如 42、true

  • 右值引用
    Rvalue references are a new reference type introduced in c++11 that help solve the problem of unnecessary copying and enable perfect forwarding.
    When the right-hand side of an assignment is an rvalue, then the left-hand side object can steal resources from the right-hand side object rather than performing a separate allocation, thus enabling move semantics.
    右值引用可以解决不必要的copy、并能进行完美转发。可以理解为在进行赋值操作时,左值偷了右值引用的资源,不必要进行额外的分配。

  • 左值持久,右值短暂;移动右值,拷贝左值;

  • 引入一种额外的引用类型当然增加了语言的复杂性,在类中将会引入移动构造函数和移动拷贝函数,即move constructor 和 move assignment。

  • 右值引用和容器类型一起使用时,将会大大提高容器的效率。如vector.insert(T &x),vector.insert(T &&x);

std::move

std::move函数可以以非常简单的方式将左值引用转换为右值引用。

  • C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制。
    这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。新版本可以使用emplace_back等函数。

  • std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能。

  • 对指针类型的标准库对象并不需要这么做。

  • 标准库代码:

template <class _Tp>
inline constexpr
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{
    
    
    typedef _LIBCPP_NODEBUG_TYPE typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

std::forwarding,完美转发

Perfect forwarding allows you to write a single function template that takes n arbitrary arguments and forwards them transparently to another arbitrary function.

  • 不完美的转发:
    经过froward函数后,本来是右值的,却调到了左值函数上去。
    在这里插入图片描述

  • 标准库代码

template <class _Tp>
inline constexpr _Tp&&
forward(typename remove_reference<_Tp>::type& __t) noexcept
{
    
    
    return static_cast<_Tp&&>(__t);
}

template <class _Tp>
inline constexpr _Tp&&
forward(typename remove_reference<_Tp>::type&& __t) noexcept
{
    
    
    static_assert(!is_lvalue_reference<_Tp>::value,
                  "can not forward an rvalue as an lvalue");
    return static_cast<_Tp&&>(__t);
}

自定义String类

示例,内部含义char*指针表示字符串,包括构造函数、析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值等函数。

#include <cstring>
#include <iostream>

using namespace std;

class String {
    
    
public:
    String(const char *cstr = nullptr);

    // 拷贝构造,参数添加const,即不会改变原理的str
    String(const String &str);

    // 移动构造,不能加const,因为要修改str,避免双重free
    String(String &&str) noexcept;

    // 拷贝赋值
    String &operator=(const String &str);

    // 移动赋值,不能加const,因为要修改str,避免双重free
    String &operator=(String &&str) noexcept;

    ~String();

    inline char *get_c_str() const {
    
    
        return m_data;
    };

private:
    char *m_data;
};

inline ostream &operator<<(ostream &out, const String &str) {
    
    
    return out << str.get_c_str();
}

/*
 * 构造函数
 *
 * 示例:String(); String("hello");
 */
inline String::String(const char *cstr) {
    
    
    cout << "String构造函数" << endl;

    if (cstr) {
    
    
        m_data = new char[strlen(cstr) + 1];
        strcpy(m_data, cstr);
    } else {
    
    
        m_data = new char[1];
        *m_data = '\0';
    }
}

// 析构函数,清除指针,避免内存泄露
inline String::~String() {
    
    
    cout << "String析构函数" << endl;
    delete[] m_data;
}

// 拷贝构造
inline String::String(const String &str) {
    
    
    cout << "String拷贝构造" << endl;

    m_data = new char[strlen(str.m_data) + 1];
    strcpy(m_data, str.m_data);
}

// 移动构造
inline String::String(String &&str) noexcept: m_data(str.m_data) {
    
    
    cout << "String移动构造" << endl;

    str.m_data = NULL; // 重要,避免双重free,即浅移动
}


// 拷贝赋值
inline String &String::operator=(const String &str) {
    
    
    cout << "String拷贝赋值" << endl;

    // 检测自我赋值,必须要添加,否则程序出错
    if (this == &str) {
    
    
        return *this;
    }

    delete[] m_data;
    m_data = new char[strlen(str.m_data) + 1];
    strcpy(m_data, str.m_data);
    return *this;
}

// 移动赋值
inline String &String::operator=(String &&str) noexcept {
    
    
    cout << "String移动赋值" << endl;
    
	// 检测自我赋值,必须要添加,否则程序出错
    if (this == &str) {
    
    
        return *this;
    }
    
    delete[] m_data;
    
    m_data = str.m_data;
    str.m_data = NULL;

    return *this;
}

RAII

Resource Acquisition Is Initialization,即资源获取即初始化。其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。
在RAII的指导下,C++把底层的资源管理问题提升到了对象生命周期管理的更高层次。
即在类的构造函数中分配资源,在析构函数中释放资源。这样,当一个对象创建的时候,构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用资源,资源的使用是安全可靠的。

C++ RAII体现出了简洁、安全、实时的特点:

  • 概念简洁性:让资源(包括内存和非内存资源)和对象的生命周期绑定,资源类的设计者只需用在类定义内部处理资源问题,提高了程序的可维护性
  • 类型安全性:通过资源代理对象包装资源(指针变量),并利用运算符重载提供指针运算方便使用,但对外暴露类型安全的接口
  • 异常安全性:栈语义保证对象析构函数的调用,提高了程序的健壮性
  • 释放实时性:和GC相比,RAII达到了和手动释放资源一样的实时性,因此可以承担底层开发的重任

智能指针

在这里插入图片描述

int main() {
    
    
	return 0;
}

猜你喜欢

转载自blog.csdn.net/u014099894/article/details/122810357