前言

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++ 好优美啊!

END