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

子类对象可以赋值给父类对象,但父类对象不能赋值给子类对象
子类对象指针不能指向父类对象,父类对象指针可以指向子类对象,但只能访问子类中父类具有的成员
多继承与虚拟继承
多继承
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释放父类指针所指的子类对象,那么只会执行父类的析构函数,而不会执行子类的析构函数,子类对象的内存就会发生泄露,而采用虚析构函数后,那么就会执行子类的析构函数

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

常用库
通用库
- Boost
- Folly:Facebook
- Abseil:Google
消息
- zeromq:消息队列
- asio:网络异步通信
- muduo:基于Reactor模式的现代C++网络库
- protobuf:消息定义和序列化
- libevent:事件通知库
多媒体
- FFmpeg:音频
- OpneCV:图像处理
- OpenGL:图形渲染
数学计算
- Eigen:线性代数
- GLM:三维系统的几何数学库
其他
- JSON: rapidjson、nlohmann/json
- 日志: spdlog、/glog