C++ 入门到放弃

3,274 阅读21分钟

C++目前在一些领域处于垄断地位,比如数据库内核、高性能网络代理、基础软件设施 等基本都是C/C++的垄断领域,虽然其他语言也有在做,但是生态、性能等都无法企及,其次C/C++有着丰富的生态,很多高级语言也提供了接口可以对接C/C++ (JNI/CGO等) ,这样你可以很方便的将一些底层C/C++库链接到自己的项目中,避免造轮子!本人学习C++目的是为了看懂别人的代码,因为很多优秀的项目都是C++写的,而非我从事C++相关领域开发!

个人觉得 C++比较难的是 内存管理 + 编译工具 + 面向对象 了,其他就是庞大且复杂的语法/模版需要勤加练习和使用,C++灵魂就是指针+模版,本文主要就是点到为止!

本篇文章会长期更新和补充,而且篇幅过长,我平时喜欢把学习语言的文档归类到一起,所以会存在体积较大的问题!

学习环境

个人觉得如果你是一个新手,一定要选一个利于学习的环境,个人比较推荐新手用 clion!其次本文全部都是基于 C++17 走的,目前C++ 版本有 98、11、14、17、20 !编译工具用的gcc + cmake,camke学习成本并不是太高(bazel复杂度有点高),可以看我写的文章: cmake入门

  • 11 基本上提供了大量的API和语法( 模版 + stl + 智能指针 )
  • 14、17 优化了语法和新增部分API,所以如果不用c++20最好的选择就是c++17了!
  • 20 支持了 coroutine(无栈协程)、concept(鸭子类型)、模块(目前还没大量使用,主要是解决编译问题)
  • 整体来看,c++20已经是一个可以媲美rust的语言了!

如果你是c++开发同学最好选择自己公司的编译工具和开发规范!C++规范,按照公司的来即可,如果没有的话可以参考Google的:github.com/google/styl…

学习文档的话,语法学习仅建议学习官方文档:en.cppreference.com/w/cpp/langu… 原因就是内容最全面、分类最具体,如果你东看西看可能概念很模糊!技巧学习的话我建议多看看开源项目,其次就是看一下经验的书 effective c++,,实践才是硬道理!

从hello world 开始

#include <iostream>

int main() {
    std::cout << "Hello"
              << " "
              << "World!" << std::endl;
}

不清楚大家对于上面代码比较好奇的是哪里了?比如说我好奇的是为啥<< 就可以输出了, 为啥还可以 << 实现 append 输出? 对,这个就是我的疑问!

思考一下是不是等价于下面这个代码了?是不是很容易理解了就!可以把 operator<< 理解为一个方法名! 具体细节下文会讲解!

#include <iostream>

int main() {
    std::operator<<(std::cout,"Hello").operator<<(" ").operator<<("World!").operator<<(std::endl);
}

内置类型

注意C++很多时候都是跨端开发,所以具体基础类型得看你的系统环境,常见的基础类型你可以直接在 en.cppreference.com/w/cpp/langu… 这里查看 !

image-20230406180051539

char* 和 char[] 和 std::string

本块内容可以先了解一遍,看完本篇内容再回头看一下会理解一些!

字符串在编程中处于一个必不可少的操作,那么C++中提供的 std::string 和 char* 区别在呢了?

简单来说const char* xxx= "字面量" 的性能应该是最高的,因为字面量分配在常量区域,更加安全,但是注意奥不可修改的!

char[]= "字面量" | new char[]{} 分配在栈上或者堆上非常不安全,这种需求直接用 std::vector 或者 std::array 更好!

std::string 在C++11有了移动语意后,性能已经在部分场景优化了很多,进行字符串操作比较多的话介意用这个,别乱用std::string* 。使用 std::string 一般不会涉及到内存安全问题,无非就是多几次拷贝! 如果用指针最好也别用裸指针,别瞎new,可以用智能指针,或者参数[引用]传递!

下面是一个简单的例子,可以参考学习!

#include <cstring>
#include <iostream>

using namespace std;

const char* getStackStr() {
    char arr[] = "hello world";
    // 不能这么返回,属于不安全的行为,因为arr分配在栈上,你返回了一个栈上的地址,但是这个函数调用这个栈就消亡了,所以不安全!
    return arr;
}

const char* getConstStr() {
    // 不会有内存安全问题,就是永远指向常量池的一块内存
    // 对于这种代码,我们非常推荐用 const char*
    const char* arr = "hello world";
    return arr;
}

const char* getHeapStr() {
    // stack 分配在栈上, 将数据拷贝到返回函数 arr上!
    char stack[] = "hello world";
    char* arr = new char[strlen(stack) + 1]{};
    strcpy(arr, stack);
    *arr = 'H';
    arr[1] = 'E';

    // arr 分配在堆上,我们返回了一个裸指针,用户需要手动释放,不释放有内存安全问题
    return arr;
}

// 这里std::string直接分配在堆上, 它的回收取决于 std::unique_ptr 的消亡, 具体有兴趣可以看下智能指针
// 注意: 千万别用函数返回一个裸指针,那么它是非常不安全的,需要手动释放!
std::unique_ptr<std::string> getUniquePtrStr() {
    auto str = std::unique_ptr<std::string>(new std::string("hello world."));
    str->append(" i am from heap and used unique_ptr.");
    return str;
}

// 注意: 这里返回的str实际上进行了一次拷贝,实现在std::string的拷贝构造函数上!
std::string getStdStr() {
    std::string str = "hello world.";
    str += " i am from stack and used copy constructor.";
    return str;
}

int main() {
    // a是一个指针,指向常量区, "hello world" 分配在常量区,对于这种申明C++11推荐用 const 标记出来,因为常量区我们程序运行时是无法修改的
    char* a = "hello world";

    // b是一个指针,指向常量区,"c++" 分配在常量区
    // 常量区编译器会优化,也就是说 a 和 b 俩人吧他们的内容都一模一样,那么所以常量只有一份
    const char* b = "hello world";
    const char* c = "hello world c";
    printf("%p\n", a);
    printf("%p\n", b);
    printf("%p\n", c);

    // arr 分配在栈上,当函数调用结束就销毁了!
    char arr[] = "1111";

    // 乱码!!!
    cout << getStackStr() << endl;
    // 正常
    cout << getConstStr() << endl;

    // 常量是不会重复分配内存的,所以下面3个输出结果是一样的!
    auto arr1 = getConstStr();
    auto arr2 = getConstStr();
    printf("%p\n", arr1);
    printf("%p\n", arr2);
    printf("%p\n", b);

    auto arr3 = getHeapStr();
    // 正常打印
    cout << arr3 << endl;
    // 需要手动释放
    delete[] arr3;

    // std::string 是一个类,也就是说它内存开销非常的高,而且对于大的数据会分配在堆上性能以及效率会差一些!

    // 这里本质上调用的是 str的copy constructor函数,属于隐式类型转换!
    std::string str = arr1;
    cout << str << endl;
    printf("%p\n", str.data());

    // 业务中如何使用 std::string了,最好使用std:unique_ptr,可以减少内存的拷贝!

    // c++ 中一般不推荐return一个复杂的数据结构(因为涉及到拷贝,
    // 或者你就用指针,或者C++11引入了移动语意,降低拷贝),而是推荐通过参数把返回变量传递过去,进而减少拷贝!
    cout << *getUniquePtrStr() << endl;
    cout << getStdStr() << endl;
}

注意点

关于 x++ 和 ++x

首先学过Java/C的同学都知道,x++ 返回的是x+1之前的值, ++x返回的是x+1后的值! 他俩都可以使x加1,但是他俩的返回值不同罢了!

#include <iostream>

using namespace std;

// 实现 x++
int xadd(int& x) {
    int tmp = x;
    x = x + 1;
    return tmp;
}

// 实现 ++x
const int& addx(int& x) {
    x = x + 1;
    return x;
}

int main() {
    int x = 10;
    //    int tmp = x++;
    int tmp = xadd(x);
    cout << "x: " << x << ", tmp: " << tmp << endl;

    // reset
    x = 10;
    //    tmp = ++x;
    tmp = addx(x);
    cout << "x: " << x << ", tmp: " << tmp << endl;

    tmp = tmp + 1;
    cout << "x: " << x << ", tmp: " << tmp << endl;
    // 输出:
    // x: 11, tmp: 10
    // x: 11, tmp: 11
    // x: 11, tmp: 12
}

引用 (左值/右值/万能引用)

引用本质上就是指针,但是它解决了空指针的问题,我个人觉得他是一个比较完美的解决方案!

  1. 下面是一个简单的例子,可以看到引用的效果 (单说引用一般是指的左值引用)
void inc(int& a, int inc) {
    a = a + inc;
}

using namespace std;
int main(int argc, char const* argv[]) {
    int a = 1;
    inc(a, 10);
    cout << a << endl;
    return 0;
}
// 输出:
// 11
  1. 其实上面这个例子(inc函数)属于左值引用,为什么叫左值引用,是因为它只能引用 左值 , 你可以理解为左值 是一个被定义类型的变量,那么它一定可以被取址(因为左引用很多编译器就是用的指针去实现的), 右值则相反,例如字面量; 右值包含纯右值将亡值(将亡值我个人理解是如果没有使用那么下一步就被回收了,生命到达终点的那种!)
int x = 10;
// x: 是一个变量,其内存分配在栈空间上,为左值,我可以取x的指针,那么x指针指向的就是栈上的某个空间
// 10: 是一个字面量,为右值,如果没有x那么它就和谁也没关系,认为是垃圾(注意右值引用就是要用垃圾,让垃圾生命延续)
  1. 下面是一个左值(lvalue)/右值(rvalue)/万能引用(Universal Reference)在实际开发中的例子
#include <unordered_map>
#include <string>
#include <iostream>
#include <shared_mutex>

// 注意: -std=c++17

template <typename K, typename V>
struct SafeMap {
public:
    // T 为万能引用(注意: 万能引用会涉及到类型推断, 区别于右值引用)
    // 万能引用一定要和std::forward(万能转发)结合使用, 不然没啥意义
    template <class T>
    auto Get(T &&key) {
        std::shared_lock lock(mutex);
        return map[std::forward<T>(key)];
    }

    // K 为右值引用
    bool Exist(K &&key) {
        std::shared_lock lock(mutex);
        if (const auto &kv = map.find(key); kv == map.end()) {
            return false;
        }
        return true;
    }

    void Put(K key, const V &value) {
        std::unique_lock lock(mutex);
        map[key] = value;
    }
    auto Size() {
        std::shared_lock lock(mutex);
        return map.size();
    }

private:
    std::unordered_map<K, V> map;
    std::shared_mutex mutex;
};

template <typename T>
using StringSafeMap = SafeMap<std::string, T>;

int main() {
    std::string key = "1";
    StringSafeMap<int> map;
    map.Put("1", 1);

    std::cout << map.Get("1") << std::endl; // 右值引用
    std::cout << map.Get(key) << std::endl; // 左值引用

    // map.Exist(key); // 编译不过去,因为没有定义左值引用函数
    if (map.Exist("1")) {
        std::cout << "exist" << std::endl;
    } else {
        std::cout << "not exist" << std::endl;
    }
}

有兴趣的可以看文章:

补充:

  • 并不是所有函数返回值都是右值,例如函数可以返回一个左值,例如 ++x
  • 右值引用可以降低内存拷贝,也就是部分情况不需要内存拷贝!
  • 万能引用可以减少代码量,尤其是参数多的情况下!

类的初始化函数

类的基本的成员函数

这个是C++ 最难的地方,新手做到知道即可,不建议深挖,无底洞一个,显然禁止拷贝和移动才是最佳选择!

C++ 的类,最基本也会有几个部分组成,就算你定义了一个空的类,那么它也会有(前提你使用了这些操作),和Java的有点像!

  • default constructor: 默认构造函数
  • copy constructor: 拷贝构造函数 (注意: 编译器默认生成的拷贝构造函数是浅拷贝!)
  • copy assignment constructor: 拷贝赋值构造函数
  • deconstructor: 析构函数
  • C++11引入了 move constructor (**移动构造函数 **) 、 move assigment constructor(移动赋值构造函数),你不定义是不会生成的。

下面例子我是根据微软的教程写的,大概可以解释6个函数 coliru.stacked-crooked.com/a/ae31c28f8…

// g++ -std=c++17 -O0 -Wall main.cpp -o main && ./main
#include <iostream>

struct Memory {
public:
    // 构造函数
    explicit Memory(size_t size) : size_(size), data_(new char[size]) {
        std::cout << "Memory constructors" << std::endl;
    }
    // 析构函数
    ~Memory() {
        clearMemory();
        std::cout << "Memory deconstructors" << std::endl;
    }
    // 拷贝构造函数(就是创建一个b,把a拷贝到b)
    Memory(Memory &from) : size_(from.size_), data_(new char[from.size_]) {
        // 参数: start,end,dst
        std::copy(from.data_, from.data_ + from.size_, data_);
        std::cout << "Memory Copy constructors" << std::endl;
    }
    // 拷贝赋值函数(就是做拷贝,把a拷贝到b)
    Memory &operator=(const Memory &from) {
        // 很可能自身移动, 这里一般都需要这么处理
        if (this == &from) {
            std::cout << "Memory Copy assignment constructors (=)" << std::endl;
            return *this;
        }
        std::cout << "Memory Copy assignment constructors (!=)" << std::endl;
        // 1. 清理自己
        clearMemory();
        // 2. 拷贝
        this->size_ = from.size_;
        this->data_ = new char[from.size_];
        std::copy(from.data_, from.data_ + from.size_, data_);
        return *this;
    }

    // 移动构造函数
    Memory(Memory &&from) noexcept : size_(0), data_(nullptr) {
        std::cout << "Memory Move constructors" << std::endl;
        // https://blog.csdn.net/p942005405/article/details/84644069
        // 1. std::move 强制变成了右值
        // 2. 调用移动赋值构造函数
        *this = std::move(from);
    }
    // 移动赋值构造函数, 这些函数需要 noexcept
    Memory &operator=(Memory &&from) noexcept {
        if (this == &from) {
            std::cout << "Memory Move Assignment constructors(=)" << std::endl;
            return *this;
        }
        std::cout << "Memory Move Assignment constructors(!=)" << std::endl;
        // 先清理自己的内存
        delete[] data_;

        // 移动
        data_ = from.data_;
        size_ = from.size_;

        // 标记空,防止析构函数失败
        from.data_ = nullptr;
        from.size_ = 0;
        return *this;
    }

    // 这里没记录写偏移量,所以这里就只支持set函数了.
    void Set(const char *data, size_t size) {
        if (size > size_) {
            size = size_;
        }
        std::copy(data, data + size, data_);
    }
    friend std::ostream &operator<<(std::ostream &out, Memory &from) {
        if (from.data_ == nullptr) {
            return out << "null";
        }
        return out << from.data_;
    }

private:
    void clearMemory() {
        if (this->data_ == nullptr) {
            return;
        }
        delete[] this->data_; // 正常来说如果不存在移动可以这么写
        this->data_ = nullptr;
    }

private:
    size_t size_{};
    char *data_{};
};

Memory getMemory(bool x) {
    if (x) {
        Memory mm(16);
        mm.Set("true", 4);
        return mm;
    }
    Memory mm(16);
    mm.Set("false", 5);
    return mm;
}

int main() {
    Memory memory1(20); // constructor
    memory1.Set("hello world", 11);
    std::cout << "memory1: " << memory1 << std::endl;

    Memory memory2 = memory1; // copy constructor (这个属于编译器优化了, 不然你这个代码也执行不通哇,因为我们没有默认构造函数, 所以左值是无法初始化的)
    std::cout << "memory2: " << memory2 << std::endl;

    Memory memory3(0); // constructor
    memory3 = memory2; // copy assignment constructor
    std::cout << "memory3: " << memory3 << std::endl;

    Memory memory4 = getMemory(true); // move constructor(如果未定义移动构造函数,则会调用拷贝构造函数,所以移动构造函数是不会默认生成的)
    std::cout << "memory4: " << memory4 << std::endl;

    memory3 = getMemory(false); // move constructor + move assignment constructor
    std::cout << "memory3: " << memory3 << std::endl;

    Memory memory5 = std::move(memory3);
    std::cout << "memory3: " << memory3 << std::endl;
    std::cout << "memory5: " << memory5 << std::endl;
    return 0;
}

总结:拷贝可以避免堆内存随意引用问题,比如我定义了A对象,此时我在A对象上分配了10M空间,此时B对象拷贝自我,那么此时B引用了A的10M内存,此时A/B回收的时候到底要清理A还是B的10M内存了? 第二个就是移动解决的问题,对于一些右值可能会存在冗余拷贝的问题,此时就可以使用移动优化内存拷贝。 本质上这些构造函数都是为了解决一个问题内存分配!!

初始化列表

这里我们要知道一点就是 C++ 类的初始化内置类型(builtin type)是不会自动初始化为0的,但是类类型(非指针类型)的话却会自动调用默认构造函数,具体为啥了,兼容C,不然会很慢,因为假如你要初始化一个类,例如定义了10个内置类型的字段,我需要10次赋值调用才能把10个字段初始化成0,而不初始化只需要开辟固定的内存空间即可,可以大大提高代码运行效率!

大部分情况下都是推荐使用初始化列表的!

#include <iostream>

// struct Info{
//     int id;
//     long salary;
// };

using namespace std;

class Demo {
  public:
    int id;
    Demo() {
        cout << "init demo" << endl;
    }
};

class Info {
  public:
    int id;
    long salary;
    Demo wrapper;
};

int main() {
    // 未使用初始化列表
    Info info;
    cout << info.id << endl;
    cout << info.wrapper.id << endl;
    int x;
    cout << x << endl;
    Info* infop;
    cout << infop << endl;
    // 使用初始化列表

    cout << "======= C++11 初始化列表 " << endl;

    Info info1{};
    cout << info1.id << endl;
    cout << info1.wrapper.id << endl;
    int x1{};
    cout << x1 << endl;
    Info* infop1{};
    cout << infop1 << endl;
}

// 输出
// init demo
// 185313075
// 88051808
// 32759
// 0x10b11c010
// ======= C++11 初始化列表
// init demo
// 0
// 0
// 0
// 0x0

类的初始化列表:

类的初始化写法

C++11 就下面这三种写法

  • ( expression-list ) 小括号括起来的表达式列表
  • = expression 表达式
  • { initializer-list } 大括号括起来的表达式列表,C++11比较推荐这种写法

然后这三种写法大题分为了几大类,这几大类主要是为了区分吧,我个人觉得就是语法上的归类,主要是cpp历史包袱太重了,其次追求高性能,进而分类了很多初始化写法,具体可以看官方文档: en.cppreference.com/w/cpp/langu…

类的多态

前期先掌握基本语法吧,实际用到的时候再深入学习,类的继承在C++中特别复杂,因为会涉及到模版、类型转换、虚函数、析构函数,注意事项非常多!

继承

下面是一个继承的例子,注意c++是支持多继承的,具体原因自行百度!

#include <iostream>

using namespace std;
class A {
  public:
    virtual void Print() {
        cout << "A::Print" << endl;
    }
    void BasePrint() {
        cout << "A:BasePrint::Print" << endl;
    }
};

// 继承A类
class B : public A {
  public:
    virtual void Print() {
        cout << "B::Print" << endl;
    }
    void BasePrint() {
        cout << "B:BasePrint::Print" << endl;
    }
};

// 继承A类
class D : public A {
  public:
    virtual void Print() {
        cout << "D::Print" << endl;
    }
    void BasePrint() {
        cout << "D:BasePrint::Print" << endl;
    }
};

// 继承B类
class E : public B {
  public:
    virtual void Print() {
        cout << "E::Print" << endl;
    }
    void BasePrint() {
        cout << "E:BasePrint::Print" << endl;
    }
};

int main() {
    A a;
    B b;
    E e;
    D d;

    A* pa = &a;
    B* pb = &b;
    D* pd = &d;
    E* pe = &e;

    pa->Print(); // A::Print
    pa->BasePrint(); // A:BasePrint::Print

    pa = pb;
    pa->Print(); // B::Print
    pa->BasePrint(); // A:BasePrint::Print
    pb->BasePrint(); // B:BasePrint::Print

    pa = pd;
    pa->Print(); // D::Print
    pa->BasePrint(); // A:BasePrint::Print
    pd->BasePrint(); // D:BasePrint::Print

    pa = pe;
    pa->Print(); // E::Print
    pa->BasePrint(); // A:BasePrint::Print
    pe->BasePrint(); // B:BasePrint::Print
    return 0;
}

override 、final

override(重写) 和 overload(重载) 区别在于 override 是继承引入的概念!

这俩修饰词主要是解决继承中重写的问题!

  1. 类被修饰为 final
class A final {
   public:
    void func() { cout << "我不想被继承" << endl; };
};

class B : A { // 这里会被编译报错,说A无法被继承!
    
};
  1. 方法被修饰为 final
class A {
   public:
    virtual void func() final { cout << "我不想被继承" << endl; }; // 申明我这个函数无法被继承,注意: final只能修饰virtual函数
};

class B : A {
  public:
    void func(); // 这里编译报错,无法重写父类方法
};
  1. 方法修饰为 override
class A {
};

class B : A {
    void func() override; // 这里编译报错,重写需要父类有定义!
};

protected

public 和 private其实没多必要介绍, 但是涉及到继承,仅允许我的子类访问那么就需要protected关键词了,区别于Java的protected.

friend

friend (友元)表示外部方法可以访问我的private/protected变量, 正常来说我定义一个一些私有的成员变量,外部函数调用的话,是访问不了的,但是友元函数可以,例如下面这个case:

#include <iostream>

class Data {
    friend std::ostream& operator<<(std::ostream& os, const Data& c);

   private:
    int id{};
    std::string name;
};

std::ostream& operator<<(std::ostream& os, const Data& c) {
    os << "(Id=" << c.id << ",Name=" << c.name << ")";
    return os;
}

int main() {
    std::cout << Data{} << std::endl;  // 这里会涉及到运算符重载的一些细节,具体可以看本篇文章!
}

指针的一些细节

注意:别瞎new指针, new了地方要么用智能指针自动回收,要么用delete手动回收! 手动new的一定会分配在堆上,所以性能本身就不高,推荐用智能指针 + raii

什么叫指针,可以理解为就是一块内存区域的地址,这个地址就是一个64/32位的无符号整数,可以通过操作这个内存地址进行 获取值(因为指针是有类型的),修改内存等操作!

在C/C++ 语言中,表示指针很简单,例如 int* ptr 表示ptr是一个int类型的指针 或者 一个int数组!

判断指针为空用 nullptr !

int main(int argc, char const* argv[]) {
    using namespace std;
    int num = 10;
    int* ptr;   // 表示ptr是一个int类型的指针
    ptr = &num; // 取num的地址
    num = *ptr; // 取ptr的值
    if (ptr) {  // 判断ptr不为空, 也可以与 nullptr 或 NULL 进行比较,我比较推荐与 nullptr 比较,比较直观!
        cout << "ptr is not nil" << endl;
    }
    int* ptr2 = nullptr;
    if (!ptr2) { // 判断ptr2为空
        cout << "ptr2 is  nil" << endl;
    }
}
// 输出:
// ptr is not nil
// ptr2 is nil

例子1: 数组与指针

C++/C 中数组和指针最奇妙,原因是 数组指针 基本概念等价,因为两者都是指向内存的首地址,区别在于数组名定义了数组的长度,但是指针没有数组长度的概念,因此我们无法通过一个指针获取数组长度!

类似于下面这个例子, arr 是一个数组,p1、p2是一个数组指针

int main(int argc, char const* argv[]) {
    int arr[] = {1, 2, 3, 4, 5};
    int* p1 = arr;
    int* p2 = &arr[0];
    cout << "sizeof(arr)=" << sizeof(arr) << ", sizeof(arr[1])=" << sizeof(arr[1]) << ", sizeof(p1)=" << sizeof(p1) << ", sizeof(p2)=" << sizeof(p2) << endl;
    cout << "arr len=" << sizeof(arr) / sizeof(arr[0]) << endl;
    cout << "arr=" << arr << ", p1=" << p1 << ", p2=" << p2 << endl;
    for (int i = 0; i < 5; i++) {
        cout << "i=" << i << ", (arr+i)=" << arr + i << ", (p1+i)=" << p1 + i << ", arr[i]=" << arr[i] << ", *(p1+i)=" << *(p1 + i) << endl;
    }
    return 0;
}

输出

sizeof(arr)=20, sizeof(arr[1])=4, sizeof(p1)=8, sizeof(p2)=8
arr len=5
arr=0x7ff7bd9999f0, p1=0x7ff7bd9999f0, p2=0x7ff7bd9999f0
i=0, (arr+i)=0x7ff7bd9999f0, (p1+i)=0x7ff7bd9999f0, arr[i]=1, *(p1+i)=1
i=1, (arr+i)=0x7ff7bd9999f4, (p1+i)=0x7ff7bd9999f4, arr[i]=2, *(p1+i)=2
i=2, (arr+i)=0x7ff7bd9999f8, (p1+i)=0x7ff7bd9999f8, arr[i]=3, *(p1+i)=3
i=3, (arr+i)=0x7ff7bd9999fc, (p1+i)=0x7ff7bd9999fc, arr[i]=4, *(p1+i)=4
i=4, (arr+i)=0x7ff7bd999a00, (p1+i)=0x7ff7bd999a00, arr[i]=5, *(p1+i)=5

结论:

  1. 数组、数组指针其实都是 数组的第一个元素对应的内存地址(指针)
  2. 数组+1 和 指针+1 ,其实不是简单的int+1的操作,而是偏移了类型的长度,原因是 指针是有类型的,且指针默认重载了 + 运算符
  3. 数组是可以获取数组的长度的,但是数组指针不可以!

注意:

  • 数组delete 和 delete[] 需要特别注意,因为 delete[]与new[] 成对出现,以及 delete和new成对出现

例子2: 数组长度

通常,我们不可能在main函数里写代码,是不是,我们更多都是函数调用,那么问题来了? 函数调用如何安全的操作呢?

int* get_array() {
    int* arr = new int[12];
    for (int i = 0; i < 12; i++) {
        *(arr + i) = i + 1;
    }
    return arr;
}
int main(int argc, char const* argv[]) {
    int* arr = get_array();
    for (int i = 0; i < 12; i++) { // 这里无法获取数组指针 arr 的长度
        cout << *(arr + i) << endl;
    }
    return 0;
}

问题: 如何获取arr的长度的呢? 显然是不可以获取的!

例子3: 常量指针

  1. 常量指针(Constant Pointer),表示的是指针指向的内存(内容)不可以修改,也就是说 *p 不可以修改,但是 p 可以修改
int const* p; // const 修饰的是 *p, *p不可以变(指向的内容),但是p可以变
const int* p; // 写法上没啥区别, 都修饰的是 *p, 我比较推荐这种写法

例子

int main(int argc, char const* argv[]) {
    using namespace std;
    int x = 10;
    int* p2 = new int;
    const int* p = &x;
    // *p = 10; // 不允许改变 指针指向的值
    p = p2; // 允许
    cout << "p: " << *p << endl;
    return 0;
}

// 输出:
// p: 0
  1. 指针常量(pointer to a constant:指向常量的指针),表示 p 不可以修改,但是 *p 可以修改
int* const p

例子

int main(int argc, char const* argv[]) {
    using namespace std;
    int x = 10;
    int* p2 = new int;
    int* const p = &x;
    *p = 20; // 允许改变 指针指向的值
    // p = p2; // 不允许
    cout << "p: " << *p << endl;
    return 0;
}

// 输出
// p: 20
  1. 指向常量的常量指针
const int* const p; // 它兼容了两者的全部优点!
  1. 总结

大部分case都是使用常量指针,因为指针传递是不安全的,如果我们的目的是不让指针去操作内存,那么我们就用 常量指针,对与指针本身来说就是一个64位的int它变与不变你不用管!

补充一些小点

  1. 指针到底写在 类型上好 int* p,还是变量上好 int *p, 没有正确答案,我是写Go的所以习惯写到类型上!具体可以看 www.zhihu.com/question/52…
  2. 指向成员的指针运算符: (比较难理解,个人感觉实际上就是定义了一个指针 alies )
    1. .* 和 ->*
    2. ::*

智能指针

在C++11中存在四种智能指针:std::auto_ptrstd::unique_ptrstd::shared_ptr std::weak_ptr

auto_ptr : c++98 中提供了,目前已经不推荐使用了

unique_ptr: 这个对象没有实现拷贝构造函数,所以我们用的时候只能用 std::move 进行移动赋值 ,经常使用

shared_ptr: 其实类似于GC语言的对象,他通过引用计数【循环引用会导致内存泄露】,实现自动回收,经常使用吧!

weak_ptr: 本质上就是解决 shared_ptr 循环引用的问题,它持有 shared_ptr,但是不会使得shared_ptr引用计数增加,很少使用吧!

c++14新增了make_unique 的api,这里的原理会涉及到 std::movestd::forward 函数相关知识, 有兴趣可以了解下 完美转发和万能引用,以及移动语意!

std::unique_ptr

class Test {
   public:
    explicit Test(int x_) : x(x_) {}
    ~Test() {
        std::cout << "release: " << x << std::endl;
    }

   public:
    int x;
};

Test* newTestFunc(int x) {
    return new Test(x);
}

int main() {
    std::unique_ptr<Test> test1 = std::unique_ptr<Test>(new Test(1));
    // unique_ptr只有移动语意,没有拷贝语义
    auto test2 = std::move(test1);
    std::cout << "test1 is null ptr: " << (test1 == nullptr) << std::endl;
    std::cout << "test2.x: " << test2->x << std::endl;

    // reset 会先释放原来指针,然后再赋值
    test2.reset(new Test(2));

    // 释放引用, 例如理论上 test2 会在main函数结束后会释放,但是我其实想要这个内容,我自己管理,就可以用 release 函数释放指针
    Test* test2_ = test2.release();
    std::cout << "test2_->x: " << test2_->x << std::endl;
    delete test2_;

    // 不推荐这么写,这样裸指针很危险,也容易忘记释放.
//    Test* test3 = newTestFunc(3);
  
    // 推荐使用智能包装一层
    auto test = std::unique_ptr<Test>(newTestFunc(3));
}

std::shared_ptr

shared_ptr 实际上基本已经对标主流的垃圾回收语言了,它使用引用计数的方式实现了垃圾回收!

shared_ptr 会存储一个引用计数器+指针,每次拷贝都会使得计数器+1然后再拷贝数据,当调用析构函数(或者 reset函数)的时候会使得计数器-1;当为0的时候会直接会去释放指针!所以原理并不复杂吧!

struct AStruct;
struct BStruct;

struct AStruct {
    std::shared_ptr<BStruct> bPtr;
    ~AStruct() { std::cout << "AStruct is deleted!" << std::endl; }
};

struct BStruct {
    int Num;
    ~BStruct() { std::cout << "BStruct is deleted!" << std::endl; }
};

void setAB(const std::shared_ptr<AStruct> &ap) {
    std::shared_ptr<BStruct> bp(new BStruct{});
    std::cout << "bp->count[0]: " << bp.use_count() << std::endl; // 1
    bp->Num = 111;
    ap->bPtr = bp;
    std::cout << "bp->count[1]: " << bp.use_count() << std::endl;         // 2
    std::cout << "bp->count[1.1]: " << ap->bPtr.use_count() << std::endl; // 2

    // defer: bp释放 count=1, 未触发回收;
}

void Test() {
    std::shared_ptr<AStruct> ap(new AStruct{});
    setAB(ap);
    std::cout << ap->bPtr->Num << std::endl;
    std::cout << "bp->count[2]: " << ap->bPtr.use_count() << std::endl; // 1

    // defer ap 释放 -> bp释放后 bp.count=0 释放bp!
}

int main() {
    Test();
}

但是这个也注定有一个陷阱,就是循环引用无法解决!

struct AStruct;
struct BStruct;

struct AStruct {
    std::shared_ptr<BStruct> bPtr;
    ~AStruct() { std::cout << "AStruct is deleted!" << std::endl; }
};

struct BStruct {
    std::shared_ptr<AStruct> aPtr;
    ~BStruct() { std::cout << "BStruct is deleted!" << std::endl; }
};

void TestLoopReference() {
    std::shared_ptr<AStruct> ap(new AStruct{});
    std::shared_ptr<BStruct> bp(new BStruct{});
    ap->bPtr = bp;
    bp->aPtr = ap;
  // 无法释放 ap 和 bp
}

int main() {
    TestLoopReference();
}

std::weak_ptr

weak_ptr 本质上并不能算的上是一个智能指针,只能说是为了解决 shared_ptr 循环引用的问题 [不能根本解决],weak_ptr相当于拷贝了一份 shared_ptr, 但是引用次数并不会增加,为此假如 shared_ptr 已经被释放了,那么weak_ptr也会指向空指针!

struct AStruct;
struct BStruct;

struct AStruct {
    std::weak_ptr<BStruct> bPtr;
    ~AStruct() { std::cout << "AStruct is deleted!" << std::endl; }
};

struct BStruct {
    std::weak_ptr<AStruct> aPtr;
    int Num;
    ~BStruct() { std::cout << "BStruct is deleted!" << std::endl; }
};

void TestLoopReference() {
    std::shared_ptr<AStruct> ap(new AStruct{});
    std::shared_ptr<BStruct> bp(new BStruct{});
    bp->Num = 1;

    // weak ptr 本身就是弱引用,此时只是只要ap/bp生命周期(也就是这个函数没执行结束)没结束就一直可以使用!
    ap->bPtr = bp;
    bp->aPtr = ap;
    std::cout << "BStruct.Num: " << ap->bPtr.lock()->Num << std::endl;
}

int main() {
    TestLoopReference();
}

智能指针和数组

  1. 针对于数组指针, 需要自己定义delete函数 coliru.stacked-crooked.com/a/83d4d163a…
  2. 针对于数组,无需特殊处理 coliru.stacked-crooked.com/a/b3e9c0103…
#include <memory>
int main() {
    // 数组
    std::shared_ptr<Int[]> data{};
    data.reset(new Int[10]);

    // 数组指针
    std::shared_ptr<Int> data2{};
    data.reset(new Int[10], [](auto p) {
        delete[] p; // 首地址的前8字节(64位)地址就是数组长度,所以可以删除成功
    });
  
    // 发现个很神奇的地方,删除数组是从尾到首部删除...
}

内存回收的一些思考

  1. 虽然C++中提供了 raii 和 智能指针,但是内存的频繁分配和频繁销毁,会给cpu造成一些开销(性能慢、延时高等),那么业务中经常遇到那种巨型结构进行序列化反序列化,那么业内也有一些解决方案,就是使用 arena ,具体可以参考

关键词

const

常量表示不可变的意思,最直接的表达就是,我这个变量初始化后你就不能进行赋值操作了!区别于其他语言,其他语言const不能用于函数的参数申明,但是C++可以,现在很多语言都可以了,主要表达的意思就是 这个参数 不可以做任何修改!

上文实际中讲到了 常量指针 和 指针常量的区别,所以也不太多解释了,const修饰的是const右边的值

这里主要是介绍一个双重指针,其他疑问可以看这个链接: www.zhihu.com/question/43…

#include <iostream>

void foo1() {
    using namespace std;
    int* x = new int(10);
    int* const* p = &x; // 表示*p是常量
    cout << **p << endl;
    **p = 100; // **p允许修改
    cout << **p << endl;
    // *p = x2; // *p不允许修改!
}

void foo2() {
    using namespace std;
    const int* x = new int(10);
    const int** p = &x; // 表示**p是常量, 因为它也不需要要用常量*x初始化, 不然编译报错!
    cout << **p << endl;
    *p = new int(11); // *p可以修改
    cout << **p << endl;
    // **p = 10; // **p不可以修改
}

int main() {
    foo1();
    foo2();
}
  1. const 可以修饰方法的返回值
const char* getString() { return "hello"; }

int main() {
    auto str = getString();
    *(str + 1) = 'a';  // 这里编译报错,只读 str
    return 0;
}
  1. const 修饰方法的参数
void printStr(const char* str) { cout << str << endl; } // 这里无法修改str

int main() {
    printStr("1111");
    return 0;
}
  1. const 修饰方法, 表示此方法是一个只读的函数
class F {
   private:
    int a;

   public:
    void foo() const { this->a = 1; } // 编译报错,无法修改 this->a !
};

static

static 主要是内存分配的问题,在程序初始化阶段会有一个静态内存区域专门存储静态变量的,其次静态局部变量可以保证多线程安全(c++11后)!

注意: c++中static定义在头文件中会被初始化多次,不要在头文件中定义全局static变量,别误以为是static作用域失效了!

  1. 全局 静态变量、静态方法
  • 静态成员变量可以初始化,但只能在类体外进行初始化
// main.h
#include <fmt/core.h>
namespace example {
static int NumberX = 100;
static int NumberY = 200;
static int NumberZ = NumberY + 300;

static void print() {
    fmt::print("x: {}, y: {}, z: {}\n", NumberX, NumberY, NumberZ);
}

class Class {
public:
    static void print();
    static const int x;
    static int y;
    static int z;
};

} // namespace example


// main.cpp
#include <fmt/core.h>
const int example::Class::x = 1;
int example::Class::y = z + 2;
int example::Class::z = 2;
void example::Class::print() {
    fmt::print("x: {}, y: {}, z: {}", x, y, z);
}

int main() {
    example::print();
    example::Class::print();
}
// output:
// x: 100, y: 200, z: 500
// x: 1, y: 4, z: 2
  1. 全局静态方法和静态变量
#include <iostream>

int inc() {
    static int sum = 0;
    return ++sum;
}

int main(int argc, char const* argv[]) {
    std::cout << inc() << std::endl;
    std::cout << inc() << std::endl;
    return 0;
}

// 输出:
// 1
// 2
  1. 模版的静态变量
class A {
public:
    static int num;
};
int A::num = 100;
class B : public A {};
class C : public A {};

template <class T>
class AA {
public:
    static int num;
};

template <class T>
int AA<T>::num = 1;
class BB : public AA<BB> {};
class CC : public AA<CC> {};

int main() {
    printf("%p\n", &B::num);
    printf("%p\n", &C::num);

    printf("%p\n", &BB::num);
    printf("%p\n", &CC::num);
}
// output:
// 0x10f2c5030
// 0x10f2c5030
// 0x10f2c5034
// 0x10f2c5038
  1. 写一个单例对象
#include <absl/base/call_once.h>
#include <fmt/core.h>
template <class T>
class ThreadSafeSingleton {
public:
    static T &get() {
        absl::call_once(ThreadSafeSingleton<T>::create_once_, &ThreadSafeSingleton<T>::Create);
        return *ThreadSafeSingleton<T>::instance_;
    }

protected:
    static void Create() { instance_ = new T(); }
    static absl::once_flag create_once_;
    static T *instance_;
};

template <class T>
absl::once_flag ThreadSafeSingleton<T>::create_once_;

template <class T>
T *ThreadSafeSingleton<T>::instance_ = nullptr;

// C++ 11 可以这么写,因为static线程安全
template <class T>
class ConstSingleton {
public:
    static T &get() {
        static T *t = new T();
        return *t;
    }
};

struct ExampleStruct {
    std::string name;
};

using ExampleStructSingleton = ThreadSafeSingleton<ExampleStruct>;
using ExampleStructConstSingleton = ConstSingleton<ExampleStruct>;

int main() {
    ExampleStructSingleton::get().name = "hello world";
    fmt::print("name = {}\n", ExampleStructSingleton::get().name);

    ExampleStructConstSingleton::get().name = "hello world";
    fmt::print("name = {}\n", ExampleStructConstSingleton::get().name);
}

extern

  1. extern C 主要是解决C++ -> C 链接方式不得同,以及C与C++函数互相调用的问题
  2. 其他待补充!

auto 和 decltype

看这里之前建议先学习模版

auto 实际上是大部分高级语言现在都有的一个功能,就是类型推断,c++11引入auto 原因也是因为模版, 其次更加方便!

decltype 本质上也是类型推断,但是它与 auto 是俩场景,解决不同的场景的问题,非常好用,decltype并不会真正的调用函数,只是获取函数的类型,非常好用,尤其是面对复杂模版的时候!

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { // 返回类型的后置写法!
    using Sum = decltype(t + u);
    Sum s = t + u;
    s = s + 1;
    return s;
}

int main() {
    auto num = add(float(1.1), int(1));
    cout << num << endl;
    return 0;
}

上面代码,如果没有 decltype 很难去实现,如果仅用模版根本无法推断出到底返回类型是啥,可能是int 也可能是 float !

注意:

  1. decltype 最难的地方还是在于它保留了 左值/右值信息,这个就给编程带来了一定的难度!
  2. c++14 有更精简的语法,具体可以看c++14语法

using 和 typedef

看这里之前先学习模版

虽然大部分case两者差距不大,using 这里主要解决了一些case 语法过于复杂的问题!

例如 typedef 无法解决模版的问题,只能依赖于类模版去实现!

using 更加方便!

#include <iostream>
#include <list>

template <typename T, template <typename> class MyAlloc = std::allocator>
using MyAllocList = std::list<T, MyAlloc<T>>;

int main() {
    auto my_list = MyAllocList<int>{1, 2, 3, 4};
    for (auto item : my_list) {
        cout << item << endl;
    }
    return 0;
}

如果用typedef 我们只能定义一个 类

#include <iostream>
#include <list>

template <typename T, template <typename> class MyAlloc = std::allocator>
struct MyAllocList2 {
   public:
    typedef std::list<T, MyAlloc<T>> type;
};

int main() {
    auto my_list_2 = MyAllocList2<int>::type{1, 2, 3, 4};
    return 0;
}

switch & break

其实我这里就想说一点,就是switch当匹配到case后,如果case没有执行break,会继续执行下面的case,已经不管case是否匹配了!!

#include <iostream>
int main() {
    int x = 2;
    switch (x) {
        case 1: {
            std::cout << "1" << std::endl;
        }
        case 2: {
            std::cout << "2" << std::endl;
        }
        case 3: {
            std::cout << "3" << std::endl;
        }
    }
}
// output
// 2
// 3

break用法

#include <iostream>

int main() {
    int x = 2;
    switch (x) {
        case 1: {
            std::cout << "1" << std::endl;
        }
        case 2: {
            std::cout << "2" << std::endl;
        }
            break;
        case 3: {
            std::cout << "3" << std::endl;
        }
    }
}
// 输出:
// 2

操作符重载(运算符重载)

本质上操作符重载就是可以理解为方法的重载,和普通方法没啥差别!但是C++支持将一些 一元/二元/三元的运算符进行重载!

实际上运算符重载是支持 类内重载、类外重载的,两者是等价的!但是有些运算符必须要类内重载,例如 =[]()-> 等运算符必须类内重载!

这也就是为啥 ostream 的 <<仅仅重载了部分类型,就可以实现输出任意类型了(只要你实现了重载),有别于一些其他语言的实现了,例如Java依赖于Object#ToString继承,Go依赖于接口实现等!运算符重载的好处在于编译器就可以做到检测!

#include <iostream>
using namespace std;
class Complex {
  private:
    int re, im;

  public:
    Complex(int re, int im) : re(re), im(im) {
    }
    // 语法就是 type operator<operator-symbol>(parameter-list)
    Complex operator+(const Complex& other) {
        return Complex(this->re + other.re, this->im + other.im);
    }
    void print() {
        cout << "re: " << re << ", im: " << im << endl;
    }
};

int main(int argc, char const* argv[]) {
    Complex a = Complex(1, 1);
    Complex b = Complex(2, 2);
    Complex c = a + b;
    c.print();
    return 0;
}

// 输出:
// re: 3, im: 3

lambda

首先lambda 其实在函数式编程很常见,但实际上我个人还是不理解,如果为了更短的代码,我觉得毫无意义,只不过是一个语法糖罢了,本质上C++的Lambda就是语法糖,编译后会发现实际上是一个匿名的仿函数!

那么什么才是lambda?我觉得函数式编程,一个很强的概念就是(anywhere define function)任意地方都可以定义函数,例如我现在经常写Go,我定义了一个方法,我需要用到某个方法,但是呢这个作用范围我不想放到外面,因为外面也用不到。因此分为了立即执行函数和变量函数

type Demo struct {
	Name *string
}

func foo() {
	newDemo := func(v string) *Demo { // newDemo变量 是一个函数类型
		return &Demo{
			Name: func(v string) *string {
				if v == "" {
					return nil
				}
				return &v
			}(v), // 立即执行函数
		}
	}
	demo1 := newDemo("1")
	demo2 := newDemo("")
	fmt.Println(demo1.Name)
	fmt.Println(demo2.Name)
}

那么换做C++,我怎么写呢? 是的如此强大的C++完全支持, 哈哈哈哈!注意是C++11 !

struct Demo {
  public:
    const char* name;
    Demo(const char* name) : name(name) {
    }
};

void foo() {
    auto newDemo = [](const char* name) {
        return new Demo([&] {
            if (*name == '\0') {
                const char * null;
                return null;
            }
            return name;
        }());
    };
    Demo* d1 = newDemo("111");
    Demo* d2 = newDemo("");
    std::cout << d1->name << std::endl;
    std::cout << d2->name << std::endl;
}

基于上面的例子我们大概知道了如何定义一个 变量的类型是函数 , 其次如何定义一个立即执行函数!

  • 函数类型
/**
[=]:通过值捕捉所有变量
[&]:通过引用捕捉所有变量
[&x]只通过引用捕捉x,不捕捉其他变量。
[x]只通过值捕捉x,不捕捉其他变量。
[=, &x, &y]默认通过值捕捉,变量x和y例外,这两个变量通过引用捕捉。
[&, x]默认通过引用捕捉,变量x例外,这个变量通过引用捕捉。
[&x, &y]非法,因为标志符不允许重复。
*/
int add1(int x, int y) {
    auto lam = [&]() { // [&] 表示引用传递
        x = x + 1;
        y = y + 1;
        return x + y;
    };
    return lam();
}

int add2(int x, int y) {
    auto lam = [=]() { // [=] 表示值传递,不可以做写操作,类似于const属性
        // x = x+1; // 不可以操作
        // y = y+1; // 不可以操作
        return x + y;
    };
    return lam();
}

int add3(int x, int y) {
    // &x表示传递x的引用
    // y 表示函数参数
    // 类型是: std::function<int(int)>
    std::function<int(int)> lam = [&x](int y) {
        x = x + 1;
        return x + y;
    };
    return lam(y);
}
  • 立即执行函数
int main(int argc, char const* argv[]) {
    // lam: 函数类型
    std::function<int(int, int)> lam = [](int a, int b) { return a + b; };
    std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl;

    // 立即执行函数
    [] { std::cout << "立即执行函数" << std::endl; }();
    return 0;
}

// 输出:
// 10 8
// 立即执行函数
  • 函数作为参数传递
std::function<void()> print(std::string str) throw(const char*) {
    if (str == "") {
        throw "str is empty";
    }
    return [=] { std::cout << "print: " << str << std::endl; };
}

int main(int argc, char const* argv[]) {
    try {
        print("")();
    } catch (const char* v) {
        std::cout << "函数执行失败, 异常信息: " << v << std::endl;
    }
    print("abc")();
    return 0;
}

// 输出: 
// 函数执行失败, 异常信息: str is empty
// print: abc

注意点:

  • 区别于仿函数,仿函数是重载了()运算符,仿函数本质上是类,但是C++11引入了 std::function 也就是 lamdba 简化了仿函数,所以C++11 不再推荐仿函数了!
  • 区别于函数指针
#include <algorithm>
#include <iostream>
#include <vector>

class NumberPrint {
   public:
    explicit NumberPrint(int max) : max(max){};
    void operator()(int num) const { // 仿函数
        if (num < max) {
            std::cout << "num: " << num << std::endl;
        }
    };

   private:
    int max;
};
void printVector(std::vector<int>&& vector, void (*foo)(int)) { std::for_each(vector.begin(), vector.end(), foo); }

void printNum(int num) { std::cout << "num: " << num << std::endl; }

int main() {
    printVector(std::vector<int>{1, 2, 3, 4}, printNum);
    auto arr = std::vector<int>{1, 2, 3, 4};
    std::for_each(arr.begin(), arr.end(), NumberPrint(3));
}
  • 函数指针的致命缺陷, 就是函数指针不支持捕获参数,所以最好别用函数指针,除非对接C!

image-20230825180008956

枚举

C++的枚举继承了C,也就是支持 enum 和 enum class,两者的区别主要是在于作用范围的不同, 例如下面 ChildStudent 都定义了 Girl 和 Body,如果不是 enum class 的话则会报错!

#include <iostream>
#include <map>

// 允许指定类型
enum class Child : char {
    Girl, // 不指定且位置是第一个就是0
    Boy = 1,
};

const static std::map<Child, std::string> child_map = {{
                                                           Child::Girl,
                                                           "Girl",
                                                       },
                                                       {
                                                           Child::Boy,
                                                           "Boy",
                                                       }};

std::ostream& operator<<(std::ostream& out, const Child& child) { // 重载方法 << 方法
    auto kv = child_map.find(child);
    if (kv == child_map.end()) {
        out << "Unknown[" << int(child) << "]";
        return out;
    }
    out << kv->second;
    return out;
}

enum class Student {
    Girl,
    Boy
};

using namespace std;

int main() {
    Child x = Child::Boy;
    cout << x << endl;
    cout << int(x) << endl;
    cout << Child(100) << endl;
}

模版

C++的模版是其语言的灵魂,模版的强大支持包含类型限定、抽象,其灵活性非常之高!

  1. 类模版

参考文章:

#include <iostream>

using namespace std;

// doc: https://en.cppreference.com/w/cpp/language/template_parameters

// Non-type template parameter
// 这里的Non-type 可以理解为它是一个具体类型,而非 'typename T' or 'class T'
// 其中类型可以是,具体参考上面文章就行了
template <int size>
struct IntArray {
    int arr[size];
};

// Type template parameter
// 这里可以是 'typename T' or 'class T'
template <typename K, typename V>
struct KV {
    K key;
    V value;
};

// 模版实例化: 偏特化
// 偏特化: 仅特化 type template parameter
template <typename K>
struct KV<K, int> {
    K key;
    int value;
    static const int type_id = 1;
};

// 模版实例化: 全特化
// 全特化: 特化全部的 type template parameter
template <>
struct KV<int, int> {
    int key;
    int value;

    static const int type_id = 2;
};

// 函数模版,此处打印 type_id
// 注意模版在编译器会检查,如果没有用到就不会检查语法错误
template <typename T>
void do_print_type_id(T t) {
    cout << "type_id: " << T::type_id << endl;
}

template <typename T, size_t S>
struct TArray {
    T array[S];
};

// Template template parameter
// 实际上个人感觉吧比较适合于, 限定 template parameter
template <typename Key, typename Value, size_t Size = 16, template <typename E, size_t S> class ArrayValue = TArray>
struct ArrayKV {
    Key key;
    ArrayValue<Value, Size> value;
};

int main() {
    KV<int, int> kv1{};
    KV<const char *, int> kv2{};
    KV<const char *, const char *> kv3{};

    do_print_type_id(kv1); // type_id: 2
    do_print_type_id(kv2); // type_id: 1
    do_print_type_id(kv3); // error: 编译失败

    ArrayKV<const char *, int> kv4{};
    kv4.value.array[0] = 1;
    kv4.value.array[1] = 2;
}
  1. 函数模版
#include <iostream>
using namespace std;

// 函数模版
template <typename K, typename V>
void do_print(K key, V value) {
    cout << "func1" << endl;
}

// 全特化 模版
// 注意函数模版不需要手动申明 '<模版实参>' (如果出现推断冲突的情况下也可以申明)
template <>
void do_print(const char *key, int value) {
    cout << "func2" << endl;
}

// 重载
// 理论上的偏特化, 但是函数支持重载, 因此函数不支持偏特化
template <typename K>
void do_print(K key, int value) {
    cout << "func3" << endl;
}

int main() {
  	// 如果出现歧义,例如2/3/4代码,其实就是歧义了,可以通过申明模版实参解决
    do_print("1", "1");                  // func1
    do_print<const char *, int>("1", 1); // func2
    do_print<const char *>("1", 1);      // func3
    do_print("1", 1);                    // func3
}
  1. 注意
  • 模版由于其特殊性实现部分只能在头文件中定义,主要原因是因为函数模版的实例化过程实际上在编译过程中,我们很多情况下仅依赖头文件和链接(编译后的产物),所以实现部分必须定义在头文件,我们才可以使用头文件的模版!
  • 或者通过模版全特化的方案
  • 其次就是参考 fmt 包的方案
  1. class 与 template 区别

有点面试经感觉,说实话我个人觉得只是语法的差异: liam.page/2018/03/16/…

高级玩法

模版的类型推断是非常强大,那么基本上高级玩法都差不多,我们可以看以下几个例子

  1. 移除指针, 头文件 type_traits 的内容

实现思路很简单,就是模版在匹配的时候,我们把指针引用的类型拿到就行了

template <class _Tp> struct _LIBCPP_TEMPLATE_VIS remove_pointer                      {typedef _LIBCPP_NODEBUG_TYPE _Tp type;};
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS remove_pointer<_Tp*>                {typedef _LIBCPP_NODEBUG_TYPE _Tp type;};
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS remove_pointer<_Tp* const>          {typedef _LIBCPP_NODEBUG_TYPE _Tp type;};
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS remove_pointer<_Tp* volatile>       {typedef _LIBCPP_NODEBUG_TYPE _Tp type;};
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS remove_pointer<_Tp* const volatile> {typedef _LIBCPP_NODEBUG_TYPE _Tp type;};

2. 限定类型,这个用处最多,他是通过参数2来限定参数1的类型,例如下面仅允许指针类型

#include <type_traits>
#include <fmt/core.h>

template <typename T>
struct IsPoint {};


// 只有指针类型: 才有value字段,且value为true
template <typename T>
struct IsPoint<T *> {
    static const bool value = true;
};

template <bool Enable, typename T = void>
struct EnableIf {};

// 仅有Enable=true的时候,才会有type申明
template <typename T>
struct EnableIf<true, T> {
    typedef T type;
};

template <typename T>
// std::is_pointer<T>::value 相当于如果你是指针,那么会有一个value=true
// std::enable_if<bool,T=void> 当true的时候会定义一个type类型,且这个type默认是void类型
void do_print_point(T t, typename std::enable_if<std::is_pointer<T>::value>::type * = nullptr) {
    fmt::print("{}\n", *t);
}

int main() {
    int x1 = 10086;
    do_print_point(&x1);

    do_print_point(x1); // 编译失败
}
  1. 模版实际上并不会检查语法,因此可以限定函数执行,具体编译后函数可以看: 链接
template <class InputIterator, class Function>
Function ForEach(InputIterator _first, InputIterator _last, Function _f) {
    for (; _first != _last; ++_first) { _f(*_first); }
    return _f;
}

int main() {
    int arr[10]{};
    ForEach(arr, &arr[9], [](int &x) { x = 10; });
    ForEach(arr, &arr[9], [](const int &x) { std::cout << x << std::endl; });
}

STL

STL:(Standard Template Library)叫做C++标准模版库,其实可以理解为C++最核心的部分,很多人望而却步,其实我感觉还好!

主要包含:

  1. 容器类模板: 基本的数据结构,数组、队列、栈、map、图 等,如果你学习过很多高级语言,那么对于C++这些容器结构我觉得其实不用太投入,只要熟悉几个API就可以了!

img

// 头文件
#include <vector>
#include <array>
#include <deque>
#include <list>
#include <forward_list>
#include <map>
#include <set>
#include <stack>
  1. 算法(函数)模板:基本的算法,排序和统计等 , 其实就是一些工具包
// 头文件
#include <algorithm>
  1. 迭代器类模板:我觉得在Java中很常见,因为你要实现 for each 就需要实现 iterator 接口,其实迭代器类模版也就是这个了!
// 头文件
#include <iterator>
  1. 总结
#include <iostream>
#include <algorithm>  // 算法
#include <iterator>  // 迭代器
#include <vector>    // 容器

// 找到targetVal位置,并在targetVal前面插入insertVal
// 未找到则在尾部插入
template <typename C, typename V>
void findAndInsert(C& container, const V& targetVal, const V& insertVal) {
    // 迭代器
    using std::begin;
    using std::end;

    // 算法
    auto it = std::find(begin(container), end(container), targetVal);
    container.insert(it, insertVal);
}

int main() {
    // 定义容器
    auto arr = std::vector<int>{1, 2, 3, 4};
    findAndInsert(arr, 4, 2);
    // 算法
    std::for_each(arr.begin(), arr.end(), [](decltype(*arr.begin()) elem) { cout << elem << endl; });
    return 0;
}
  1. 现在很多高级语言都支持切片,可以说是大大提高了开发效率,但是cpp也有,也很简单,区别在于是c++实现的是拷贝,而非内存复用,所以这种需求还是用迭代器比较好!
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>

using IntVector = std::vector<int>;

template <typename T>
void print(std::vector<T> &v) {
    int index = 0;
    std::cout << "[";
    for (const auto &item : v) {
        if (index != 0) { std::cout << ", "; }
        std::cout << item;
        ++index;
    }
    std::cout << "]" << std::endl;
}

int main() {
    IntVector v1{};
    v1.reserve(10);
    for (int x = 0; x < 10; x++) { v1.push_back(x); }
    print(v1);
    // [:4]
    auto v2 = IntVector(v1.begin(), v1.begin() + 4);
    print(v2);
    // [1:-1]
    auto v3 = IntVector(v1.begin() + 1, v1.end() - 1);
    print(v3);

    // 更加推荐,传递迭代器
    for (auto begin = v1.begin(); begin != v1.begin() + 4; begin++) {
        std::cout << "range: " << *begin << std::endl;
    }

    // c++14支持lambda表达式参数用auto
    std::for_each(v1.begin() + 1, v1.begin() + 4, [](auto elem) { std::cout << "for_each: " << elem << std::endl; });
}

预处理器 - 宏

宏本质上就是在预处理阶段把宏替换成对应的代码,属于代码模版[ C++/C 思想真的超前 ],可以省去不少代码工作量,其次就是性能更好,不需要函数调用,直接预处理阶段内联到代码中去了,例如我这里就用了宏 github.com/Anthony-Don…

宏的玩法太高级,很多源码满满的宏,不介意新手去深入了解!只要能看懂就行了,简单实用一下也完全可以的哈!

简单的例子

#include <iostream>

#define product(x) x* x

using namespace std;
int main() {

    int x = product((1 + 1)) + 10; // 展开后:  (1 + 1)*(1 + 1) + 10
    std::cout << "x: " << x << std::endl;

    int y = product(1 + 1) + 10; // 展开后: 1 + 1*1 + 1 + 10
    std::cout << "y " << y << std::endl;

#ifdef ENABLE_DEBUG
    cout << "print debug" << endl;
#endif
}

// 输出:
x: 14
y: 13

class+宏+类名的意义

注意: 这里要是有windows环境的话可以自己体验下!

不清楚大家阅读过c++源码吗,发现开源的代码中基本都有一个 ,那么问题是 PROTOBUF_EXPORT 干啥了?

class PROTOBUF_EXPORT CodedInputStream {
  	//...
}

实际上你自己写代码没啥问题,定不定义这个宏,你要把代码/ddl提供给别人用windows的开发者来说就有问题了,别人引用你的api需要申明一个 __declspec(dllexport) 宏定义,表示导出这个class,具体可以看 learn.microsoft.com/en-us/cpp/c… 所以说对于跨端开发来说是非常重要的这点!

其次这个东西很多时候可以在编译器层面做手脚,表示特殊标识,反正 大概你知道 windows 下需求这个东东就行了!

#define DllExport   __declspec( dllexport )

class DllExport C {
   int i;
   virtual int func( void ) { return 1; }
};

RTTI

待补充!

多线程

en.cppreference.com/w/cpp/threa…

cpp11 的 thread、mutex、lock_guard、lock_uniq、feature

cpp14 支持了 shared_lock

cpp17 支持了 async 、shared_mutex

cpp20 支持了 jthread 和 coroutine

#include <mutex>
#include <thread>
#include <iostream>

int main() {
    using GuardLock = std::lock_guard<std::mutex>;
    using UniqueLock = std::unique_lock<std::mutex>;
    std::mutex mutex;
    int count = 0;
    {
        auto test = [&count, &mutex]() {
            for (int y = 0; y < 100000; y++) {
                GuardLock lock(mutex);
                ++count;
            }
            std::cout << std::this_thread::get_id() << ": " << count << std::endl;
        };
        std::jthread tt(test);
        std::jthread t2(test);
        std::jthread t3(test);
    }
    std::cout << "main" << count << std::endl;
}

其他

new 与 malloc

我们知道,我们可以再 C语言里使用 mallocfrees 初始化内存,但是C++ 里更加推荐使用 new 和 delete ,那么区别在哪里了!

首先我们知道C++引入了 构造函数 和 析构函数,因此我们用 c系列的api操作,会丢失这些信息,这就是最主要的区别,也是特别需要注意的!

例子一: 最常见的乱用行为!

class Test {
   public:
    explicit Test(int x_) : x(x_) {}
    ~Test() {
        std::cout << "release: " << x << std::endl;
    }

   public:
    int x;
};

int main() {
    Test* test = new Test(1);
    // 错误行为
//    free(test);

    // 正确,会调用析构函数!
    delete test;
}

例子二: 业务中为了做一些事情,例如有些特殊case需要用 void* 指针进行操作(例如导出C),解决内存拷贝的问题

struct CClass {
    void* point;
};

int main() {
    Test* test = new Test(1);
    CClass c{
        .point = test,
    };
    // 正确行为,需要强制转换成 Test*;
    delete (Test*)c.point;
}

new[] 与 delete[]

我们可以简单看下面这个例子,就大概明白了,new与delete的区别

class Test {
   public:
  	Test() = default;
    int x;
};

void builtin() {
    auto list = new int[10]{};
    for (int x = 0; x < 10; x++) {
        list[x] = x;
    }
    cout << *((unsigned long*)list - 1) << endl; // 输出不确定
    delete[] list;
}

void external() {
    auto list = new Test[10]{};
    for (int x = 0; x < 10; x++) {
        list[x].x = x;
    }
    cout << *((unsigned long*)list - 1) << endl; // 输出10
    delete[] list;
}
  1. 内置类型的话,内存中不会存储长度字段

image-20230517005229594

  1. 其他类型,会在首地址-8 的位置存储长度,也就是64位是8字节

image-20230517005016406

  1. 所以对于 new[] 的指针对象,一定要用delete[] 释放,不然的话你会内存泄漏奥!

C++ 位域

learn.microsoft.com/zh-cn/cpp/c… 可以节约内存开销

#include <iostream>

using namespace std;

struct http_parser {
    /** PRIVATE **/
    unsigned int type : 2;                   /* enum http_parser_type */
    unsigned int flags : 8;                  /* F_* values from 'flags' enum; semi-public */
    unsigned int state : 7;                  /* enum state from http_parser.c */
    unsigned int header_state : 7;           /* enum header_state from http_parser.c */
    unsigned int index : 5;                  /* index into current matcher */
    unsigned int uses_transfer_encoding : 1; /* Transfer-Encoding header is present */
    unsigned int allow_chunked_length : 1;   /* Allow headers with both
                                              * `Content-Length` and
                                              * `Transfer-Encoding: chunked` set */
    unsigned int lenient_http_headers : 1;

    uint32_t nread;          /* # bytes read in various scenarios */
    uint64_t content_length; /* # bytes in body. `(uint64_t) -1` (all bits one)
                              * if no Content-Length header.
                              */
    /** READ-ONLY **/
    unsigned short http_major;
    unsigned short http_minor;
    unsigned int status_code : 16; /* responses only */
    unsigned int method : 8;       /* requests only */
    unsigned int http_errno : 7;
    /* 1 = Upgrade header was present and the parser has exited because of that.
     * 0 = No upgrade header present.
     * Should be checked when http_parser_execute() returns in addition to
     * error checking.
     */
    unsigned int upgrade : 1;
    /** PUBLIC **/
    void *data; /* A pointer to get hook to the "connection" or "socket" object */
};

int main() {
    cout << sizeof(http_parser) << endl;
}
// output:
// 32

namespace

我们知道c语言是没有namespace的概念的,作用域是全局的,所以导致头文件如果存在公共定义是可能会存在问题的。

c++ 支持了namespace,解决命名冲突的问题,但是同样的它会造成编译的时候 符号连接会带上namespace,导致c语言无法和c++链接,此时就需要用到 extern "C" 了!

namespace Misc {
namespace Utils {
struct Consts {
};
} // namespace Utils
} // namespace Misc

namespace Misc {
namespace Network {
struct TcpConnect {
    using Consts = Utils::Consts; // 他们都存在Misc namespace下,所以可以这么引用,因为必须要全限定namespace(这种日常开发中经常会使用)

    using FullConsts = Misc::Utils::Consts; // 不推荐
};
} // namespace Network
} // namespace Misc

常用库

其他学习资料