目录

C++笔记

编译流程

.c文件经过 预编译 ,将宏和头文件展开,生成 .i 文件,之后经过 编译 ,将文件编译成汇编语言,生成 .s 文件,再经过 汇编 ,将文件汇编成目标文件 .o ,最后经过 链接 ,生成可执行文件

语法笔记

对C语言的加强

三目运算符

简短判断可用三目运算符 a>b ? a : b
C中三目运算符返回的是值,而C++返回的是变量本身

const

遵循就近修饰原则

const int a = 1;  //修饰int,即int型的值不可修改,也即a不可修改,等同于int const a = 1;
const int* b = &p;     //修饰int,即int型的值不可修改,故b指针指向的值不可修改,但b本身可以修改,可以指向其他const int型的变量
int * const c = &q;    //修饰*;即指针型的值不可修改,故c的值不可修改,但其指向的数据的值可以修改

const必须进行初始化
注:C中的const不是真正的const,其可通过指针进行改变。C++中的const则如何都不会改变,为真正意义上的常量
const与#define的区别

enum枚举

enum season{
    SPR,
    SUM,
    AUT,
    WIN
};

enum season s = SPR;

C++特性

封装、继承、多态

引用

引用必须进行初始化,且只能与一个变量进行绑定,本质为一个常量指针

int &b = a;  //初始化,与a绑定
int* &p = q; //指针引用

对右值进行引用,用两个&符:
int && c = 100;

对函数的参数使用引用可以避免传参时拷贝,提升效率。

inline内联函数

适用于重复使用的简单函数,避免了重复的入栈出栈,可一定程度上提升性能
使用限制:不能存在任何的循环语句;不能存在大量的判断语句;不能对函数进行取址操作

inline void func(){
    cout << "test" << endl;
}

默认参数

默认参数定义时从后向前设置,使用时从前向后

函数重载

函数名相同,参数列表不同(参数个数、类型、顺序),使用时系统自动调用符合的函数,函数重载只看参数列表
一般参数个数相同时用函数模板

函数重载与函数指针
int func(int a, int b){
    return a+b;
}
int func(int a, int b, int c){
    return a+b+c;
}
typedef int(MY_FUNC)(int,int);     //第一种方式 定义函数类型MY_FUNC,返回值为int,参数为(int,int)
typedef int(*MY_FUNC_P)(int,int);  //第二种方式 定义函数指针类型
int(*my_func)(int,int);            //第三种方式 定义函数指针

MY_FUNC* fp1 = func;
fp1(10,20);

MY_FUNC_P fp2 = func;
fp2(10,20);

my_func fp3 = func;
fp3(10,20);

用函数指针调用的函数不能进行函数重载,因为在一开始定义函数指针时就确定了函数的参数列表

类和对象

相比于C语言中的结构体,类不仅可以定义变量,还可以定义成员函数,多了访问控制等特点

class Animal{
public:                   //public:在类的内部和外部都可以访问
    void SetKind(char kind[]);
    void SetColor(char color[]);
protected:                //在单个类中与private一样,主要在继承中起作用
private:                  //只能在类的内部进行访问
    char kind[10];
    char color[10];
}

构造和析构

构造函数和析构函数都没有返回值,且析构函数没有形参
当类中无任何构造函数(无参构造、有参构造、拷贝构造)时,系统会有一个默认的无参构造函数,当写了有参的构造函数时,则不存在默认构造函数,除非再写一个无参的构造函数

class Test{
public:
    Test(){}            //无参构造函数,即默认构造函数
    Test(int x, int y)  //构造函数,在对象创建时用来初始化的函数。有多个构造函数则会重载
    :m_x(x),m_y(y)      //初始化变量
    {
        /*
        代码
        */
    }
    ~Test(){           //析构函数,在对象回收之间调用
        cout << "析构" << endl;
    }
private:
    int m_x;
    int m_y;
};                     //记得要有分号

拷贝构造函数

class Test{
public:
    Test(int x, int y):m_x(x),m_y(y){
    }
    Test(const Test &t){            //在初始化时调用,如Test t2(t1)或Test t2 = t1,但Test t2;t2 = t1这种方式就没有调用,因为它是在用无参构造函数初始化完之后再进行的赋值的,这种情况下是用的运算符重载operator=
        m_x = t.m_x;                //若不写则调用默认的拷贝构造函数
        m_y = t.m_y;                //析构时,谁先构造谁就最后析构
        cout << "copy func" << endl;
    }
private:
    int m_x;
    int m_y;
};

拷贝构造函数在初始化时调用,如Test t2(t1)或Test t2 = t1。特别在函数传参时,也是调用拷贝构造函数,如void test(Test t);test(t1),因为传参过程相当于Test t = t1;而另一种情况下:Test t; t = t1,则为运算符重载

深拷贝与浅拷贝

浅拷贝就是默认的拷贝构造函数,对于指针两种拷贝方式才有区别,所以对于堆中的数据进行拷贝时应使用深拷贝
深拷贝则是自己写的拷贝构造函数,重新申请空间来复制保存堆中的数据
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,如果其中一个释放空间的话,那另一个指针指向的就是空指针。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

static修饰的静态成员变量

静态变量的初始化必须在类外进行,静态变量是属于类,而不属于对象,即各个对象公用这个变量
对静态变量的访问函数应定义成静态函数
在继承中,基类中定义的静态变量在派生类中共用

class Test{
public:
    static int a;
    static int getNum(){  //静态成员函数只能访问静态成员变量,因为static是属于类的,而this指针属于对象,故static中没有this指针,故访问不了非static的成员变量  
        return b;
    }
private:
    static int b;
};

public静态变量和静态函数可以直接通过类名来访问,如Test::a = 10; Test::getNum(),因为静态变量和函数是属于类的
因为静态变量和函数是属于类的,所以求类的大小并不包含在内

this指针

this指针为当前对象的地址

class Test{
public:
    int a;
    int getNum() const         //若将成员成员函数声明为const,则该函数不允许修改类的数据成员
    {
        //this->b = 10;        //此行代码不能执行,因为const修饰
        return this->b;
    }
private:
    int b;
};

友元函数与友元类

一个类的变量一般为私有成员,方法一般为公有函数,若一个不属于类中的函数需要频繁访问类中的成员,那么只能通过类的公有函数来间接访问,则导致了函数的频繁压栈出栈,导致效率较低。友元函数则允许外部函数在类外访问类的私有成员,不用通过函数进行访问,提高了效率,但同时破坏了类的封装性

友元函数
class Test{
public:
    friend int getNum(Test &T);  //声明在类的哪里都可以,定义必须在类外
    int a;
private:
    int b;
};

int getNum(Test &T){             //定义:按普通函数的定义方式即可
    return T.b;
}
友元类
class Test{
public:
    friend class A;  //声明A为友元类,A中所有的成员函数均可访问此类中的任何成员
    int a;
private:
    int b;
};

new和delete

new和delete是操作符,而不是像malloc()和free()这样的函数

int *p = new int;     //开辟一个int大小的空间
delete p;
int *q = new int[10]; //开辟10个int大小的数组
delete[] q;
Test *t = new Test(1,2) //申请一个Test对象,并进行初始化

操作符重载

操作符重载要写在全局,即main函数之外

//普通操作符返回值为临时变量
Complex operator+(Complex &a, Complex &b){   //对两个复数类型变量进行相加操作
    //代码
    return temp;
}
//+=类似的操作符返回值类型为引用,返回值为第一个操作符
Complex& operator+=(Complex &a, Complex &b){   //对两个复数类型变量进行相加操作
    //代码
    return a;
}

智能指针

auto_ptr(C++98)、unique_ptr(C++11)和shared_ptr(C++11)

#include <memory>
auto_ptr<int> ptr(new int);
auto_ptr<Test> ptr2(new Test[10])

shared_ptr、weak_ptr、unique_ptr:
shared_ptr和unique_ptr用于对对象具有拥有权的情况
unique_ptr用于对对象具有使用权观察权的情况,如观察者模式下,被观察的对象应该用weak_ptr来进行观察,而其对象本省的管理应该使用shared_ptr

继承

父类(基类)、子类(派生类)

class Test : public Test1{     //Test类继承于Test1
public:
private:
}

/images/cpp/inherit.png

子类对象可以赋值给父类对象,但父类对象不能赋值给子类对象
子类对象指针不能指向父类对象,父类对象指针可以指向子类对象,但只能访问子类中父类具有的成员

多继承与虚拟继承

多继承
class Test : public Test1, public Test2{

}
虚继承

如下代码,B1与B2均继承于A,当D多继承B1、B2时就会有2份A中的变量,而这是不必要的,为解决此问题,引入了虚继承

class A;
class B1:public  A;
class B2:public  A;
class D:public B1,public B2;

在父类继承时加入virtual关键字即可实现虚继承,尽量不要用多继承

class A;
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;

多态与虚函数

多态指的是由继承而产生的相关的不同的类,其对象会对同一消息作出不同的响应
扩展:在存在虚函数的类中,都有一个隐藏指针vptr,其指向虚函数表,记录了类中的虚函数

#include <iostream>
using namespace std;

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      //若为虚函数,则是 virtual int area(){...}
      int area()
      {
         cout << "Parent class area :" << endl;
         return 0;
      }
};
class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      {
         cout << "Rectangle class area :" << endl;
         return (width * height);
      }
};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      {
         cout << "Triangle class area :" << endl;
         return (width * height / 2);
      }
};

int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);

   // 存储矩形的地址
   shape = &rec;                 //父类指针可以指向子类地址,但只能调用父类中存在的成员,故无虚函数时,调用的就是父类中的成员,而有了虚函数时调用的就是子类中重写的成员,从而实现了多态,即不同子类对同一函数有不同的输出
   // 调用矩形的求面积函数 area
   shape->area();

   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();

   return 0;
}
------------输出结果------------
Parent class area
Parent class area

若改为虚函数,则结果为

Rectangle class area
Triangle class area
纯虚函数

只定义,不实现,在各子类中实现
拥有纯虚函数的类成为抽象类

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      //纯虚函数
      virtual int area() = 0;
};
----------或如下实现,不过麻烦,纯虚函数就是为了简化以下方法------
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }

      virtual int area();
};
int Shape::area(){
    //空函数,以后由子类重写
}

析构函数也可多态,即也可写为虚函数。虚析构函数的作用为,当一个父类指针指向子类对象时,若delete释放父类指针所指的子类对象,那么只会执行父类的析构函数,而不会执行子类的析构函数,子类对象的内存就会发生泄露,而采用虚析构函数后,那么就会执行子类的析构函数

/images/cpp/poly.png
switch和if语句也是动态联编

重载重写重定义

重载:两个函数在同一个作用域,根据参数列表来调用合适的函数
重写:虚函数,子类重写父类的函数
重定义:子类中有个与父类相同的函数,则子类对象调用时调用的是子类重定义的函数(但父类指针指向子类对象时,调用的是父类成员,但加了virtual以后,调用的就是子类成员,即上述的重写)

函数模板

函数模板只能用于函数的参数个数相同而类型不同的情况,如果参数个数不同,则不能使用函数模板
函数模板不允许自动类型转换;函数模板可与普通函数进行函数重载,也可与函数模板进行重载

定义方式:
template<typename T>
void Swap(T &a, T &b){   //或写作 template<class T>
    T temp = a;
    a = b;
    b = temp;
}
调用方式:
1 自动推导类型
Swap(a,b);
2 显式指定类型
Swap<int>(a,b);

类模板

函数模板再调用时可以自动推导类型或显式指定类型,但类模板在调用时必须显式指定类型

template<typename T>
class Test{
public:
    Test(T id, T name);
private:
    T id;
    T name;
};

使用类模板继承时,要指明父类的类型

class Test1:public Test<int>{

};

类型转换

方式 说明
static_cast 一般的转换,用于内置的数据类型(如int)和具有继承关系的指针和引用
dynamic_cast 转换具有继承关系的指针或引用,在转换前会进行对象类型安全检查,所以只能将子类转换为父类
const_cast 对指针、引用和对象指针的const转换
reinterpret_cast 用于进行没有任何关联之间的转换,比如一个字符指针转换为一个整型数

异常

int divide(int x, int y){
    if (y == 0)
        throw y;    //抛异常

    return x/y;
}

void test(){
    try{                //捕获异常
        divide(10,0);
    }
    catch (int e){  //异常时根据类型进行匹配
        cout << "异常值:"  << e << endl;
    }
    catch(...){     //捕获除以上类型外的所有异常
        cout << "其他类型异常";
    }
}

为增强程序的可读性,可在函数声明中列出可能抛出的所有异常类型

int divide(int x, int y) throw(int, char, float);  //只能抛出int,chat和float型的异常  
int divide(int x, int y) throw();                  //不可抛出异常
int divide(int x, int y);                          //可抛出所有类型的异常

输入输出

cin:标准输入,有缓冲区

//读取字符
ch = cin.get() or cin.get(ch);
//读取字符串
char buf[256];
cin.get(buf,256);
//读取一行
cin.getline(buf,256); //用getline读取一行和用get读取一行的区别在于getline不会读取换行符
//忽略当前字符,即从缓冲区中取走当前字符,注意与cin.peek()区别开
cin.ignore(n) //n为忽略字符的额个数
//读取当前缓冲区中的字符,执行完,缓冲区中还有
cin.peek()

cout:标准输出,有缓冲区

//输出字符
cout.put(ch)
//输出字符串
cout.write("hello",strlen("hello"))

cerr:标准错误,无缓冲区
clog:标准日志,有缓冲区

文件操作

文本文件和二进制文件读写的区别:文本文件是为了更方便的处理Windows和Linux下换行符的,Windows下的换行符为\r\n,而Linux下的换行符为\n,所以,对于同一个换行符在不同操作系统下的表示是不同的。所以若对Linux下的一个文本文件用二进制方式原封不动的拷贝到Windows上是不会正确显示的,若用文本文件方式,则会进行换行符的转换,以适应Windows的正确显示。对于二进制文件读写,则是将原来的文件内容不做修改地读到目标文件中

文本文件:

#include <fstream>

char *fileName = "D:\\document\\test.txt";
char *targetName = "D:\\document\\target.txt";
//打开文件
//ifstream ism(fileName,ios::in);  或用如下方式

//打开输入文件流input file stream
ifstream ism;
ism.open(fileName,ios::in);
//打开输出文件流
ofstream osm;
osm.open(targetName,ios::out); //ios::out为覆盖原文件,若想要追加内容,则应该写为ios::out | ios::app
//判断是否打开成功
if (!ism){
    cout << "打开文件失败";
}

//读文件
char ch;
while(ism.get(ch)){
    cout << ch;
    osm.put(ch);
}

//关闭文件
ism.close();
osm.close();

二进制文件:

//以二进制方式读入文件
ifstream ism(sourceFile,ios::in | ios::binary);

//以二进制方式输出文件
ofstream osm(targetName, ios::out | ios::binary);
osm.write("文件内容",sizeof("文件内容"));

osm.close();

STL标准模板库

容器、迭代器与算法:容器负责数据的存储,算法负责对数据的操作,迭代器为联系两者的桥梁

详细请参考http://c.biancheng.net/stl/

应用笔记

虚函数重写可改变其原有的访问权限

若父类的虚函数为private,在子类中重写此虚函数时可将其置为public

考虑使用RAII来管理资源

// 对比
File* in = File::Open(in_file, "r");
if (in) {
  File* out = FIle::Open(out_file, "w");
  if (out) {
    // use file
    delete out;
  }
  delete in;
}

//更清晰
std::unique_ptr<File> in = File::Open(in_file, "r");
if (!in)
  return false;
std::unique_ptr<File> out = File::Open(out_file, "w");
if (!out)
  return false;
// use file

时间转时间戳

struct tm expire_timestamp_tm;
strptime(auth_record.expire_time().c_str(), "%Y-%m-%d %H:%M:%S", &expire_timestamp_tm);
time_t expire_timestamp = mktime(&expire_timestamp_tm);

指针删除要置为nullptr

delete后要设置为null,避免重复删除报错。delete空指针不会报错,但delete非空指针则会报错

多使用clang

/images/cpp/clang.png

常用库

通用库

  • Boost
  • Folly:Facebook
  • Abseil:Google

消息

  • zeromq:消息队列
  • asio:网络异步通信
  • muduo:基于Reactor模式的现代C++网络库
  • protobuf:消息定义和序列化
  • libevent:事件通知库

多媒体

  • FFmpeg:音频
  • OpneCV:图像处理
  • OpenGL:图形渲染

数学计算

  • Eigen:线性代数
  • GLM:三维系统的几何数学库

其他

  • JSON: rapidjson、nlohmann/json
  • 日志: spdlog、/glog