前言
C++ 和其他语言的差别好大。
面向对象开发
宏定义
#ifndef xxx
#define xxx
...
#endif
为了防止重复 include,可以写:如果没有 define xxx,那么就 define,然后 include。这样在下次碰到的时候因为已经 define 过了,所以不会重复 include。
内联函数
inline int add(int a,int b)
{
return a+b;
}
inline 主要解决的是,若这个函数写在某个头文件中,此时多个 cpp 文件都 include 这个头文件,那么在链接的时候就会发生问题,将这个函数内联后就可以解决。除此之外,类内的成员函数会被编译器隐式 inline,否则多次 include 这个类就会出问题。对于性能而言,对于上述这种简单函数,即使不声明内联,编译器也可以自动内联。内联后的机器码就会减少函数调用,直接把函数内部的代码拆出来运行。
构造函数
T re;
T im;
complex(T r,T i):re(r),im(i){}
对于初始化列表的写法,此时会直接调用 re 和 im 的有参构造函数,否则就会先调用默认构造函数,再调用赋值函数。而这里,即使写成 im(i),re(r),顺序也是先 re 再 im,这个是由声明顺序决定的。
class A{
public:
static A& getInstance();
void setup(){...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}
A::getInstance().setup();
对于单例模式 Singleton,其要求整个程序中只允许存在一个对象,并通过A::getInstance() 获取它。此时会考虑将构造函数写在 private 里,使得不允许别人自己构造,而是通过 getInstance 获得唯一对象,注意拷贝构造函数也要 private。
double real() const {return re;}
这里的常量成员函数,通过在函数的括号后加 const 来限制函数内部不能修改成员变量。所以,对于常量对象,其只能调用常量成员函数。
析构函数
void* mem=operator new(sizeof(String));
pc=static_cast<String*>(mem);
pc->String::String("hello");
在调用 new 的时候,编译器会先分配内存,然后将指针转型,最后再调用对象的构造函数。
String *p=new String("hello");
delete p;
当 delete 一个指针时,会先调用指针指向对象的析构函数,然后再释放对象本身占用的资源。
内存分配
当 new 申请一块内存时,其会在原本需要的空间的基础上,头尾额外申请两块 4 字节的内存,这个叫做 cookie,用来唯一标明这一段。之后在这样计算出所需的空间后,此时需要满足申请的空间都是 16 的倍数。那么就是找到大于等于所需空间的 16 的倍数,然后申请这一块内存。
假如分配出的内存是 80,对应十六进制为 0x50,此时选择用最后一位是 1 表示这块内存已经被分配了。那么在上下 cookie 里填的就是 0x51,在释放后再次变成 0x50。
在 new 一个数组的时候,此时需要额外用 4 个字节记录分配的大小。此时在 delete[] 的时候,此时编译器知道要释放一个数组,就会对每个数组中的对象调用一次析构函数。而如果此时用 delete 而非 delete[],此时就只会调用数组中第一个对象的析构函数,从而造成内存泄漏,造成 Undefined Behavior。
具体情况会根据实际情况和编译器调整。
#ifndef My_String
#define My_String
class String
{
public:
String(const char* cstr=0)
{
if(cstr)
{
m_data=new char[strlen(cstr)+1];
strcpy(m_data,cstr);
}
else
{
m_data=new char[1];
*m_data='\0';
}
}
String(const String &str)
{
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
}
String& operator=(const String &str)
{
//self assignment
if(this==&str)
{
return *this;
}
delete[] m_data;
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
return *this;
}
~String()
{
delete[] m_data;
}
const char* get_c_str() const { return m_data; }
friend std::ostream& operator<<(std::ostream &os,const String &str)
{
os<<str.get_c_str();
return os;
}
private:
char* m_data;
};
#endif
Delegation 委托
在设计的时候,可以考虑当前任务类只提供操作接口,再另写一个能执行所有操作的实现类,然后让当前类只维护一个指向实现类的指针。当调用任务类的成员函数时,只需要通过指针对实现类进行操作,不需要在自己这里操作。
此时,对于同一个内容的不同对象,就可以让其实现类的指针全都指向同一个实现类,达到共享的效果。而一个对象发生修改时,此时需要单独给这个对象复制一份,即 copy on write 写时复制。
虚函数
class Shape{
public:
virtual void draw()const=0;
virtual void error(const std::string &msg);
int objectID()const;
};
在继承中,子类会继承父类成员函数的 调用权。此时,对于非虚函数,子类可以定义与父类同名的非虚函数,但这不是覆写 override,而是隐藏 hiding,不具有运行时多态效果。而对于虚函数,子类可以去覆写这个函数。对于纯虚函数,即上述 draw 这个 =0 的虚函数,此时子类若想实例化就必须要重新覆写。
这里,由于 id 是获得每个子类对象单纯拥有的 id,所以没必要让子类自己定义。而对于错误信息,子类可以自己去设计自己的报错提示,也可以使用这个默认的方法。而对于绘制,子类不同图形必须要自己定义自己的绘制方法,不能统一使用。
class A
{
public:
A(){
std::cout<<"A"<<std::endl;
}
};
class C
{
public:
C(){
std::cout<<"C"<<std::endl;
}
};
class B:public A
{
public:
B(){
std::cout<<"B"<<std::endl;
}
private:
C c;
};
A
C
B
这里,当子类 B 继承了父类 A,且类内组合了类 C 时,此时构造会先调用父类的构造函数,再调用组合对象的构造函数,最后再调用自己的构造函数。析构的时候就是反过来。
多态
class Component
{
public:
virtual void draw()=0;
};
class Primitive:public Component
{
public:
void draw() override
{
std::cout<<"Primitive\n";
}
};
class Composite:public Component
{
public:
void draw() override
{
std::cout<<"Composite\n";
}
};
Component* p1=new Primitive;
Component* p2=new Composite;
p1->draw();
p2->draw();
C++ 允许将子类指针自动转换成父类指针,即向上转型 upcasting,对引用也是同理。对于这里的 public 继承,此时虽然 p1 和 p2 两个指针类型都是 Component*,但实际输出的都是它们自己。这是因为 draw() 函数是虚函数,运行时会根据实际对象类型决定调用哪个版本,这个就是运行时多态。
注意,对于 Component* p1,在 delete 时如果可能通过父类指针删除子类对象,那么父类的析构函数也必须是虚函数,即 virtual ~Component()。
Composite 与 Prototype 设计模式
在组合模式 Composite 中,Primitive 和 Composite 都 public 继承 Component,Composite 内部再保存 vector<Component*>。 因此树中的 “ 叶子 ” 和 “ 容器 ” 可以统一通过 Component* 操作,并通过 virtual 实现不同行为。具体的应用场景就是文件系统,一个目录下可以放文件也可以放子目录,通过文件和目录的父类实现。
而在原型模式 Prototype 中,父类提供 virtual clone() 并保存不同子类的 “ 类名 ”,然后每次通过 Base*-> clone() 根据动态类型创建对应子类对象。相当于实现一种 “ 虚构造函数 “。
泛型编程
转换函数 conversion function
class Fraction
{
public:
Fraction(int num,int den=1):m_numerator(num),m_denominator(den){}
operator double()const
{
return (double)m_numerator/m_denominator;
}
private:
int m_numerator;
int m_denominator;
};
对于分数类,可以使用 operator double() 实现类型转换。此时,由于转换到的类型已经确实,所以是没必要也不能写返回值的。
class Fraction
{
public:
Fraction(int num,int den=1):m_numerator(num),m_denominator(den){}
Fraction operator+ (const Fraction &f)
{
...
}
};
Fraction f(3,5);
Fraction d=f+4;
这里,由于其构造函数是一个 one-argument 函数,所以在执行上面的分数相加操作时,可以调用其构造函数,将传入的 4 转为 Fraction。
class Fraction
{
public:
explicit Fraction(int num,int den=1):m_numerator(num),m_denominator(den){}
operator double()const
{
return (double)m_numerator/m_denominator;
}
Fraction operator+ (const Fraction &f)
{
...
}
private:
int m_numerator;
int m_denominator;
};
当既有 one-argument 构造函数,又有 operator double() 转换函数时,operator+ 就不知道该怎么转换了,就会发生报错。此时可以选择 explicit 关键字写为显式构造函数,此时相加就不会自动将 4 转为 Fraction 了。
Pointer-like class
智能指针
template<class T>
class shared_ptr
{
public:
T& operator* ()const
{
return *px;
}
T* operator-> ()const
{
return px;
}
shared_ptr(T* p):px(p){}
private:
T* px;
long *pn;
};
shared_ptr<Foo>sp(new Foo());
Foo f(*sp);
sp->method();
对于智能指针,就是内部保存一个真正的指针,然后要重载所有指针相关的运算符,使得其可以像指针一样被使用。operator* 可以理解,对于 operator-> 情况会比较特殊,其在智能指针返回内部的指针后,会递归地调用返回指针的 operator->,直到遇到纯指针才应用原本的语义。
多说一点,为了实现 int* p1=new int(1);int* p2=p1;*p2=2; 这种传统指针的功能,shared_ptr 还有一个共享控制块 control block,这里的 pn 指针可以抽象地理解为用来维护同一个 control block 被几个指针指向的引用计数。当发生拷贝构造时,新产生的 px 会和旧的 px 指向同一个对象,并共享同一个 control block,此时就需要通过增加引用计数来维护。
而传统指针会面临多个程序共用同一个指针时,谁都怕别人还在用而不敢 delete,最后造成内存泄漏问题。那么为了解决这个生命周期协调问题,在 shared_ptr 中就被设计成,只有当最后一个拥有该对象的 shared_ptr 释放所有权时,才会销毁被管理对象。这样就不只是复制地址,而是复制了访问对象的能力并共享这个对象的所有权,以此达到自动协调生命周期,即 “ shared_ptr ”。
迭代器
T& operator*()const
{
return (*node).data;
}
T* operator->()const
{
return &(operator*());
}
self& operator++();
self& operator--();
对于迭代器,除了上述两个重载,还需要重载自增和自减表示迭代器的移动。此外 operator* 要返回的是指向的容器内的对象。而 operator-> 由于要适配 iter->method() 的写法,要先执行 operator* 操作得到指向的容器内的对象,即 Foo&。然后再对这个对象取地址得到其指针,此时才可以接上用户的调用。
仿函数 Function-like class
template<class T>
struct Add
{
T operator()(const T& x,const T& y)const
{
return x+y;
}
};
Add<int>add;
int x=add(1,2);
当一个类重载了 operator() 函数调用运算符,其就可以被当成一个函数调用。
成员模板
template<class T1,class T2>
struct pair{
T1 first;
T2 second;
template<class U1,class U2>
pair(const pair<U1,U2>& p):first(p.first),second(p.second){}
};
pair<Derived1,Derived2>p;
pair<Base1,Base2>p2(p);
成员模板允许在当前已经推导出的模板参数的基础上,再额外接受别的模板参数。这里就是给 pair<T1,T2> 提供了模板转换构造函数,使得只要 U1 能构造 T1、U2 能构造 T2,那么就可以由 pair<U1,U2> 构造 pair<T1,T2>。
这里,父类对象可以由子类对象初始化,但如果是按值保存而非指针或引用保存,此时会发生对象切片 object slicing,即只保留子类中属于父类的部分。所以在运行多态的时候都会选择使用指针或引用,而不是按值转换。
template<class T>
class shared_ptr
{
template<class>
friend class shared_ptr;
public:
template<class U>
shared_ptr(const shared_ptr<U>& other);
private:
T* px;
long *pn;
};
shared_ptr<Derived>pd(new Derived);
shared_ptr<Base>pb(pd);
在多态里,由于允许 Base* p=new Derived,所以智能指针也必须实现这种功能。此时就需要使用成员模板提供转换构造函数,使得可以实现 shared_ptr<Base>pb(pd)。类似的,此时如果想要实现 Derived* d; Base* b=d; 这种隐式转换,就不能使用 explicit 关键字。
需要注意的是,shared_ptr<Base> 和 shared_ptr<Derived> 是两种类型,不能访问对方的私有成员,此时需要显式声明模板友元。
模板特化
template<class Key>
struct hash{};
template<>
struct hash<char>
{
...
};
对于泛化的模板,此时可以对指定类型进行特别处理。
template<typename T,typename Alloc=...>
class vector
{
...
};
template<typename Alloc=...>
class vector<bool,Alloc>
{
...
};
这里就是偏特化,即对部分模板参数进行特化。
template<typename T>
class C
{
...
};
template<typename U>
class C<U*>
{
};
这里,由于模板参数可以接受一个类型,而指针也是一个类型,所以就可以对指针作为模板参数的情况进行特化,这里就是范围上的偏特化而非个数上的偏特化。
模板模板参数
template<typename T,
template<typename T>
class Container
>
class XCls
{
private:
Container<T>c;
public:
...
};
template<typename T>
using Lst=list<T,allocator<T>>;
XCls<string,Lst>mylst;
当想要指定一个容器作为模板参数,此时还需要指定这个容器的模板参数,那么就需要使用模板模板参数。这个可以理解为,普通模板参数传一个类型,模板模板参数传一个模板。
这里由于 Container 模板模板参数是一个只有类型的模板,而 list 容器不止有类型这一个模板参数,所以需要再额外包装成一个只需要类型的模板。
可变模板 Variadic templates
void print()
{
std::cout<<"End of recursion"<<std::endl;
}
template<typename T,typename... Types>
void print(const T& firstArg,const Types&... args)
{
std::cout<<firstArg<<std::endl;
print(args...);
}
print(7.5, "Hello",42,3.14,'A');
传统的模板要求模板参数的个数必须固定,但此时可以使用 ... 对模板参数进行打包。此时外部调用 print() 的时候,执行时就会递归地调用自己,每次将包里的第一个参数输出,直到最后没有参数时调用上方的 print 结束递归。
template<typename T,typename... Types>
void print(const T& firstArg,const Types&... args)
{
std::cout<<firstArg<<std::endl;
if(sizeof...(args)!=0)
{
print(args...);
}
}
这里,也可以使用 sizeof...(args)查看包的大小。所以整个递归过程可以进一步优化,此时就不会调用上方结束的 print 了。
template<typename T,typename... Types>
void print(const T& firstArg,const Types&... args)
{
std::cout<<firstArg<<std::endl;
if constexpr (sizeof...(args)!=0)
{
print(args...);
}
}
这里注意,即使写了这句判断,由于调用到最后包为空了,而这个 print 模板函数仍然要求有至少一个参数,所以虽然不会执行但无法通过编译。此时可以选择使用 if constexpr,告诉编译器这部分可以提前在编译期确定,那么就不需要写额外无参的 print 了,因为内部不会编译到。
对象模型
虚机制
class A
{
public:
virtual void vfunc1(){}
virtual void vfunc2(){}
void func1(){}
void func2(){}
private:
int m_data1,m_data2;
};
class B:public A
{
public:
virtual void vfunc1(){}
void func2(){}
private:
int m_data3;
};
class C:public B
{
public:
virtual void vfunc1(){}
void func2(){}
private:
int m_data1,m_data4;
};
std::cout<<"Size of class A: "<<sizeof(A)<<std::endl;
std::cout<<"Size of class B: "<<sizeof(B)<<std::endl;
std::cout<<"Size of class C: "<<sizeof(C)<<std::endl;
Size of class A: 16
Size of class B: 24
Size of class C: 32
这里,和原本的静态绑定不同,在动态绑定 Dynamic Binding 中,对象里会多带一个虚指针 virtual pointer,用来指向一个虚表 virtual table。虚表中存放函数指针,指向虚函数所在位置,对虚函数进行索引。
那么由于 A 有两个虚函数,所以其虚表中就有两个函数指针,分别指向 A 的 vfunc1 和 vfunc2。而由于 B 继承了 A,所以其虚表中指向的就是 A 的 vfunc2 和 B 自己的 vfunc1。对于 C,那么就是指向自己的 vfunc1 和 B 的 vfunc2,也就是 A 的 vfunc2。
所以 A 的大小就是指针的 8 字节,加上两个 4 字节的 int 等于 16 字节。之后 B 继承 A 以后再加上 4 字节等于 20 字节,内存对齐为 24 字节。C 继承 B 的 24 字节后再加上自己的两个 4 字节 int,最后是 32 字节。
const
charT operator[](size_type pos)const{};
reference operator[](size_type pos){};
需要注意的是,const 也是函数签名的一部分,也可以发生函数重载。此时,当成员函数的 const 和非 const 版本同时存在时,非 const 对象只能调用这个非 const 成员函数。对于非 const 的函数,其会发生修改操作,所以需要考虑 COW copy on write。而加了 const 的函数不能发生修改操作,当 const 对象调用这个操作符时,其调用的就是 const 修饰的这个函数。
new 和 delete
class Foo
{
private:
int _id;
long _data;
public:
Foo():_id(0)
{
std::cout<<"Default constructor, this = "<<this<<std::endl;
}
Foo(int i):_id(i)
{
std::cout<<"Constructor, this = "<<this<<std::endl;
}
~Foo()
{
std::cout<<"Destructor, this = "<<this<<std::endl;
}
static void* operator new(size_t size)
{
Foo* p=(Foo*)malloc(size);
std::cout<<"operator new, this = "<<p<<", size = "<<size<<std::endl;
return p;
}
static void operator delete(void* pdead,size_t size)
{
std::cout<<"operator delete, this = "<<pdead<<", size = "<<size<<std::endl;
free(pdead);
}
static void* operator new[](size_t size)
{
Foo* p=(Foo*)malloc(size);
std::cout<<"operator new[], this = "<<p<<", size = "<<size<<std::endl;
return p;
}
static void operator delete[](void* pdead,size_t size)
{
std::cout<<"operator delete[], this = "<<pdead<<", size = "<<size<<std::endl;
free(pdead);
}
};
Foo* pf1=new Foo;
delete pf1;
Foo* pf2=::new Foo;
::delete pf2;
Foo* pArray=new Foo[3];
delete[] pArray;
当要自己实现一个类的 new 时,此时需要写成 static 静态成员函数。因为当调用 new 的时候,这个对象还没有被实例化,没有 this 指针,此时只能调用静态成员函数。此时的流程是,先调用 operator new 分配空间,再在这块空间上调用构造函数,然后返回指针。
而对于 delete,由于顺序是反过来,先调用析构函数,再调用 operator delete 释放内存。此时由于对象已经被析构了,所以也需要设计成 static。
当想强制使用全局的 new 和 delete 时,此时可以使用 ::new 和 ::delete。
Placement new
class Foo
{
private:
int _id;
long _data;
public:
Foo():_id(0)
{
std::cout<<"Foo::Foo()"<<std::endl;
}
Foo(int i):_id(i)
{
std::cout<<"Foo::Foo(int)"<<std::endl;
throw Bad();
}
void* operator new(size_t size)
{
std::cout<<"operator new(size_t), size = "<<size<<std::endl;
return malloc(size);
}
void* operator new(size_t size,void* start)
{
std::cout<<"operator new(size_t,void*), size = "<<size<<", start = "<<start<<std::endl;
return start;
}
void* operator new(size_t size,long extra)
{
std::cout<<"operator new(size_t,long), size = "<<size<<", extra = "<<extra<<std::endl;
return malloc(size+extra);
}
void* operator new(size_t size,long extra,char init)
{
std::cout<<"operator new(size_t,long,char), size = "<<size<<", extra = "<<extra<<", init = "<<init<<std::endl;
return malloc(size+extra);
}
void operator delete(void* pdead,size_t size)
{
std::cout<<"operator delete(void*,size_t)"<<std::endl;
}
void operator delete(void* pdead,void* start)
{
std::cout<<"operator delete(void*,void*)"<<std::endl;
}
void operator delete(void* pdead,long extra)
{
std::cout<<"operator delete(void*,long)"<<std::endl;
}
void operator delete(void* pdead,long extra,char init)
{
std::cout<<"operator delete(void*,long,char)"<<std::endl;
}
};
Foo start;
Foo* p1=new Foo;
Foo* p2=new(&start)Foo;
Foo* p3=new(100)Foo;
Foo* p4=new(100,'A')Foo;
try
{
Foo* p5=new(100)Foo(1);
}
catch(const Bad& e)
{
std::cout<<"caught: "<<e.what()<<std::endl;
}
try
{
Foo* p6=new(100,'A')Foo(1);
}
catch(const Bad& e)
{
std::cout<<"caught: "<<e.what()<<std::endl;
}
try
{
Foo* p7=new(&start)Foo(1);
}
catch(const Bad& e)
{
std::cout<<"caught: "<<e.what()<<std::endl;
}
try
{
Foo* p8=new Foo(1);
}
catch(const Bad& e)
{
std::cout<<"caught: "<<e.what()<<std::endl;
}
Foo::Foo()
operator new(size_t), size = 16
Foo::Foo()
operator new(size_t,void*), size = 16, start = 0x7fffffffdde0
Foo::Foo()
operator new(size_t,long), size = 16, extra = 100
Foo::Foo()
operator new(size_t,long,char), size = 16, extra = 100, init = A
Foo::Foo()
operator new(size_t,long), size = 16, extra = 100
Foo::Foo(int)
operator delete(void*,long)
caught: Bad exception
operator new(size_t,long,char), size = 16, extra = 100, init = A
Foo::Foo(int)
operator delete(void*,long,char)
caught: Bad exception
operator new(size_t,void*), size = 16, start = 0x7fffffffdde0
Foo::Foo(int)
operator delete(void*,void*)
caught: Bad exception
operator new(size_t), size = 16
Foo::Foo(int)
operator delete(void*,size_t)
caught: Bad exception
对于形如 Foo* p=new(...)Foo 的调用,此时这个操作被称为 Placement new。在重载时要求第一个参数必须是 size_t,只不过使用时不显式传进来。之后的参数就可以自己定义了,可以根据需要写多个重载,使用时必须把这些参数传进来。
对于相应的 delete,可以写也可以不写。这里 delete 的含义是,当在构造时发生了异常,此时的内存已经申请了,那么就需要对应的 placement delete 进行释放。如果没发生异常,那么销毁时还是调用正常的 delete,不会调用特定的 placement delete。
这里,一个很常见应用场景就是 operator new(size_t size,void* start),意思就是我已经有了 start 这块内存,在这块内存的基础上直接构造,即 " Placement " 含义。除此之外,也可以通过 operator new(size_t size,long extra) 传入一个变量 extra 额外量,然后在原本需要的空间的基础上多分配这个额外量的内存。
需要注意的是,这里的 operator new(size_t size,void* start) 由于用的是别处的内存,不是这里 new 出来的。所以此时通常需要自己管理这个对象的生命周期,即 p->~Foo() 析构对象,然后单独管理内存。
总结
C++ 好优美啊!