C++入门

189 阅读15分钟

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

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

本篇文章会长期更新和补充!

学习环境

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

如果你是c++开发同学最好选择自己公司的编译工具和开发规范!

书籍的话我不介意去看花大量时间去看 C++ Primer 这种书籍,介意掌握一定基础后直接读 effective c++,实践才是硬道理!

从hello world 开始

cpp
复制代码
#include <iostream>

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

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

那么带着问题,我们可能要熟悉 C++操作符重载 和 C++面向对象编程! 然后你就可以很好的理解上面代码了!

思考一下是不是等价于下面这个代码了?是不是很容易理解了就!可以把 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,可以用智能指针,或者参数[引用]传递!

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

cpp
复制代码
#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,但是他俩的返回值不同罢了!

cpp
复制代码
#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
}

引用 (左值/右值引用)

不介意前期看!

下面是一个简单的例子,可以看到引用的效果

cpp
复制代码
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

其实上面这个例子属于左值引用,为什么叫左值引用,是因为它只能引用 左值 , 你可以理解为左值 是一个被定义类型的变量,那么它一定可以被取址(因为左引用很多编译器就是用的指针去实现的), 右值则相反,例如字面量; 右值包含纯右值将亡值(将亡值我个人理解是如果没有使用那么下一步就被回收了,生命到达终点的那种!)

cpp
复制代码
int x = 10;
// x: 是一个变量,其内存分配在栈空间上,为左值,我可以取x的指针,那么x指针指向的就是栈上的某个空间
// 10: 是一个字面量,为右值,如果没有x那么它就和谁也没关系,认为是垃圾(注意右值引用就是要用垃圾,让垃圾生命延续)

char* s= "hello world";
//

具体可以看下面这个例子

cpp
复制代码
#include <iostream>
#include <vector>

using namespace std;

void rvalue(int&& x) {
    x = x + 2;
}

void lvalue(int& x) {
    x = x + 1;
}

void pointValue(int* x) {
    *x = *x + 1;
}

int main() {
    int number = 10;
    lvalue(number);      // number 可以通过指针查找到,是一个已经明确分配的内存!
    pointValue(&number); // number可以取指针
    // rvalue(number);              // 不可以
    rvalue(10);                  // 可以,因为10是一个字面量!
    rvalue([] { return 10; }()); // 可以,因为返回值10如果不用就当垃圾销毁了!

    // x++ 返回的是右值,额外发生了临时变量拷贝,返回的是拷贝的数字,为右值
    // lvalue(number++);
    rvalue(number++);

    // ++x 返回的是左值,直接在x上自增返回!
    lvalue(++number);
    // rvalue(++number);
}

有兴趣的可以看文章:

补充: 并不是所有函数返回值都是右值,例如函数可以返回一个左值,例如 ++x

右值引用技巧

  1. 右值引用可以解决无用的内存拷贝!右值如果你没有使用那么就是无用数据,如果我们利用起来它就是有用的值了!
  2. 通常我们需要定义 两个构造函数,一个是拷贝构造函数 、 一个是移动构造函数!

类的初始化函数

类的基本的成员函数

这个是C++ 最难的地方,新手做到知道即可,不建议深挖,无底洞一个!

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

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

补充:

  1. 默认的拷贝/拷贝赋值构造函数是浅拷贝
  2. 默认不会生成移动构造函数和移动赋值构造函数
  3. 移动 和 拷贝的区别在于,移动的本质是a指向b, 拷贝的本质是 a 要拷贝一份b的数据,会有新数据的产生
  4. std::move() 可以将一个左值改变成一个右值,在部分场景会用到!
  5. 拷贝构造函数用于 左值 或者 未定义移动构造函数的case
  6. 参考文章: paul.pub/cpp-value-c…

image-20230407121445355

例如下面 TestStrcut 实际上我重写了这几个函数,大概我们看一下它的调用逻辑吧

cpp
复制代码
#include <iostream>

using namespace std;

class TestStrcut {
  public:
    TestStrcut() { cout << "invoke default constructor" << endl; }
    TestStrcut(const TestStrcut& ts) { cout << "invoke copy constructor" << endl; }
    TestStrcut& operator=(const TestStrcut& ts) { //  注意: 这里的&不是取值符号,而是一个引用符号! https://www.cnblogs.com/haruyuki/p/15683592.html
        cout << "invoke copy assigment constructor" << endl;
        return *this;
    }
    ~TestStrcut() { cout << "invoke deconstructor" << endl; }
};

TestStrcut foo(TestStrcut ts) {
    return ts;
}

int main(int argc, char const* argv[]) {
    //  省略写法 TestStrcut ts1;
    TestStrcut ts1 = TestStrcut();

    // 省略写法 TestStrcut ts2(ts1);
    // ts2为啥了,因为初始化一个新变量的时候是不会执行 copy assigment
    // constructor的,省了一步吧!
    TestStrcut ts2 = TestStrcut(ts1);

    // ts3这里为啥了? 不直接调用 copy assigment constructor
    // 了,因为你是一个新变量,不需要考虑 = 这种操作符! 但是你需要考虑初始化!
    // 不然ts1 和 ts2都需要调用 = 操作符
    TestStrcut ts3 = ts2;
    TestStrcut ts4;

    // 这里就做了一个赋值操作,因为ts4已经初始化了,但是要把ts3赋值给ts4.
    ts4 = ts3;

    cout << "===start===" << endl;

    // 函数调用要值传递,所以要copy一下, ts4 copy给了 foo函数的参数1,
    // 函数有返回值,返回值也需要拷贝一次;
    TestStrcut ts5 = foo(ts4);
    cout << "===end===" << endl;
    return 0;
}

输出

shell
复制代码
invoke default constructor
invoke copy constructor
invoke copy constructor
invoke default constructor
invoke copy assigment constructor
===start===
invoke copy constructor
invoke copy constructor
invoke deconstructor
===end===
invoke deconstructor
invoke deconstructor
invoke deconstructor
invoke deconstructor
invoke deconstructor

总结:

用到再说!

初始化列表

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

cpp
复制代码
#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 } 大括号括起来的表达式列表

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

类的多态

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

继承

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

cpp
复制代码
#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
cpp
复制代码
class A final {
   public:
    void func() { cout << "我不想被继承" << endl; };
};

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

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

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

protected

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

friend

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

cpp
复制代码
#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的一定会分配在堆上,所以性能本身就不高!

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

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

判断指针为空用 nullptr !

cpp
复制代码
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是一个数组指针

cpp
复制代码
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;
}

输出

shell
复制代码
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. 数组是可以获取数组的长度的,但是数组指针不可以!

例子2: 数组长度

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

cpp
复制代码
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 可以修改
cpp
复制代码
int const* p; // const 修饰的是 *p, *p不可以变(指向的内容),但是p可以变
const int* p; // 写法上没啥区别, 都修饰的是 *p, 我比较推荐这种写法

例子

cpp
复制代码
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 可以修改
cpp
复制代码
int* const p

例子

cpp
复制代码
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. 指向常量的常量指针
cpp
复制代码
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: 暂时还没学习

智能指针会涉及到 std::movestd::forward 函数相关知识, 有兴趣可以了解下 完美转发和万能引用,以及移动语意!

例子

说实话单想,真想不到啥例子,因为我才刚学习C++,没有太多经验!

cpp
复制代码
#include <iostream>
#include <memory>
#include <utility>

using namespace std;

class A {
   public:
    ~A() { cout << "调用A的析构函数, 我被释放了" << endl; }

   private:
   public:
    int getNum() const { return num; }
    void setNum(int d) { A::num = d; }

   private:
    int num;
};

void printA(std::shared_ptr<A> a) { cout << "num is: " << a->getNum() << endl; }

void handler(std::shared_ptr<A> a) {
    static int num = 1;
    a->setNum(num);
    printA(a);
    num = num + 1;
}

int main() {
    auto a = unique_ptr<A>(new A());
    if (a == nullptr) {
        cout << "(1) a is null" << endl;
    }
    auto b = std::move(a);
    //    auto c = a;  // 报错
    if (a == nullptr) {
        cout << "(2) a is null" << endl;
    }
    if (b == nullptr) {
        cout << "(3) b is null" << endl;
    }

    auto aa = std::shared_ptr<A>(new A());
    auto bb = aa;
    handler(bb);
    handler(aa);
}

// 输出
// (2) a is null
// num is: 1
// num is: 2
// 调用A的析构函数, 我被释放了
// 调用A的析构函数, 我被释放了

关键词

const

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

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

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

cpp
复制代码
#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 可以修饰方法的返回值
cpp
复制代码
const char* getString() { return "hello"; }

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

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

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

static

这个如果你学过Java,static就再陌生不过了,主要差异在于初始化方式上和生命周期上!

  1. 面向对象

在面向对象,我们经常会用到一些静态类,赋予这个类一些静态方案和静态变量,那么我们就可以无需初始化这个静态类就可以使用了!

cpp
复制代码
#include <iostream>

using namespace std;

class StaticClass {
  private:
    static int counter;

  public:
    static int inc();
};

// must init
int StaticClass::counter = 0;

int StaticClass::inc() {
    return ++StaticClass::counter; // 访问静态成员变量,可以 类名::成员变量名,可以用.
}

int main(int argc, const char* argv[]) {
    cout << "inc: " << StaticClass::inc() << endl;
    cout << "inc: " << StaticClass::inc() << endl;
    return 0;
}
  1. 静态方法和静态变量

注意点就是: 静态变量和全局变量没啥区别,区别在于静态变量、方法不可以被其他文件所访问(可以防止重命名,虽然命名空间可以解决),这个需要特别注意,它有着全局变量的生命周期! 可以参考: zhuanlan.zhihu.com/p/37439983

cpp
复制代码
#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

extern

后续再补充吧,目前还不太理解!

auto 和 decltype

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

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

decltype 本质上也是类型推断,但是它与 auto 是俩场景,解决不同的问题

cpp
复制代码
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 更加方便!

cpp
复制代码
#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 我们只能定义一个 类

cpp
复制代码
#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;
}

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

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

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

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

cpp
复制代码
#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 其实在函数式编程很常见,但实际上我个人还是不理解,如果为了更短的代码,我觉得毫无意义,只不过是一个语法糖罢了!

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

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 !

cpp
复制代码
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;
}

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

  • 函数类型
cpp
复制代码
/**
[=]:通过值捕捉所有变量
[&]:通过引用捕捉所有变量
[&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);
}
  • 立即执行函数
cpp
复制代码
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
// 立即执行函数
  • 函数作为参数传递
cpp
复制代码
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 不再推荐仿函数了!
  • 区别于函数指针
cpp
复制代码
#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++的枚举继承了C,也就是支持 enum 和 enum class,两者的区别主要是在于作用范围的不同, 例如下面 ChildStudent 都定义了 Girl 和 Body,如果不是 enum class 的话则会报错!

cpp
复制代码
#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++ 中的泛型编程的基础,实际上C++泛型就是基于模版做的,叫模版原因就是实际上你写的代码就是模版,编译期会根据实际你用到的类型,给你生成对应的代码(函数模版 or 类模版),所以它不会出现Java中的泛型擦除

日常开发中我们基本上也用不到自己去定义一个模版,基本上都在公共库中存在,所以会用即可!

这里我大概介绍一下大家的困惑,实际上写模版的时候吧你看一个资料它可能写在上面,也有可能写在左侧。其次可能用class也可能用 typename ,那么到底是啥了?

注意: 模版由于其特殊性实际上只能在头文件中定义,主要原因是 涉及到C++的动态/静态链接,假如你模版类/函数 生成到动态链接库引用,头文件只定义申明,那么模版是在编译期生成的代码,导致 链接库就没有对应类型的代码,导致编译失败!

注意:还有一种解决方案就是 显示申明模版,让编译器强制给你生成这个模版类型的实现,但是也有局限性就是 tm 的不能包含全部类型哇!所以你看STL的模版库 全部是 header 头,不过还有一种方案就是 我们只要在头文件中可以拿到模版类型的基础信息就可以了,然后向下传递,参考fmt包!

cpp
复制代码
#include <iostream>

using namespace std;

// 写法1: 比较推荐的写法
template <typename T> T min(T& x, T& y) {
    return x < y ? y : x;
}

// 写法2: 模版定义在上面
// template <typename T>
// T min(T& x, T& y) {
//     return x < y ? y : x;
// }

// 写法3: 使用class关键词,后续会补充两者区别,也可以直接搜文档,https://liam.page/2018/03/16/keywords-typename-and-class-in-Cxx/
// template <class T> T min(T& x, T& y) {
//     return x < y ? y : x;
// }

int main(int argc, char const* argv[]) {
    cout << "min(1,2)=" << min(1, 2) << endl;
    cout << "min(1.1,2.2)=" << min(1.1, 2.2) << endl;
    return 0;
}
  1. class 与 template 区别 liam.page/2018/03/16/…
  • template 可以处理 嵌套依赖类型
cpp
复制代码
class Foo {
  public:
    typedef int bar_type;
};

template <typename T> 
class Bar {
  public:
    typedef typename T::bar_type bar; // 嵌套依赖类型
    bar data;
};
  • class 可以处理 模板作为模板参数 , 有点像Java的 泛型有界类型参数 本质上就是定义泛型参数的范围
cpp
复制代码
template <typename T, size_t L> 
class MyArray {
    T arr[L];

  public:
    MyArray() {
    }
};

template <typename T, size_t L, template <typename, size_t> class Arr>
class MyClass {
    T t;
    Arr<T, L> a;
};

int main(int argc, char const* argv[]) {
    auto b = MyClass<int, 10, MyArray>();
    return 0;
}
  1. 模版的一些其他用法

    1. 函数模版特化:这个类似于方法的重载,你可以理解为模版没有限制类型,但是你可以针对于某个类型去自定义实现
cpp
复制代码
using namespace std;
template <typename T> void print(T t) {
    cout << "print: " << t << endl;
}
template <> void print(int t) {
    cout << "number: " << t << endl;
}

int main() {
    print("hello");
    print(1);
    return 0;
}
// 输出
// print: hello
// number: 1

STL

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

主要包含:

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

img

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

预处理器 - 宏

宏本质上就是在预处理阶段把宏替换成对应的代码,可以省去不少代码工作量,其次就是性能更好,不需要函数调用,直接内联到代码中去了!

宏的玩法太高级,很多源码满满的宏,不介意新手去深入了解!只要能看懂就行了!

简单的例子

cpp
复制代码
#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 干啥了?

cpp
复制代码
class PROTOBUF_EXPORT CodedInputStream {
  	//...
}

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

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

cpp
复制代码
#define DllExport   __declspec( dllexport )

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

RTTI

待补充!

多线程

单独补充!

资料