1、类的代码风格
- 类访问控制块的声明依次序是 public:, protected:, private:,缩进和 class 关键字对齐
class MyClass : public BaseClass {
public: // 注意没有缩进
MyClass(); // 标准的4空格缩进
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing()
{
}
void SetVar(int var) { someVar = var; }
int GetVar() const { return someVar; }
private:
bool SomeInternalFunction();
int someVar;
int someOtherVar;
};
- 构造函数初始化列表放在同一行或按四格缩进并排多行
// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : someVar(var)
{
DoSomething();
}
// 如果不能放在同一行,
// 必须置于冒号后, 并缩进4个空格
MyClass::MyClass(int var)
: someVar(var), someOtherVar(var + 1) // Good: 逗号后面留有空格
{
DoSomething();
}
// 如果初始化列表需要置于多行, 需要逐行对齐
MyClass::MyClass(int var)
: someVar(var), // 缩进4个空格
someOtherVar(var + 1)
{
DoSomething();
}
- 使用命名空间来限定作用域
namespace Switcher {
class Packet {
...
}
void SendPacket(const Packet& packet);
}
namespace Router {
class Packet{
...
}
void SendPacket(const Packet& packet);
}
全局变量、全局常量、全局类型定义由于都属于全局作用域,在项目中使用第三方库容易出现冲突,命名空间将作用域细分为独立的具名的作用域,可有效防止全局作用域的命名冲突。
2、构造函数、析构函数、复制构造函数、赋值运算操作符
- 类的成员变量必须显式初始化
- 成员变量优先使用声明时初始化(C++11)和构造函数初始化列表初始化
- 为避免隐式转换,将单参数构造函数声明为explicit
class Foo {
public:
explicit Foo(const string& name): name(name)
{
}
private:
string name;
};
void ProcessFoo(const Foo& foo){}
int main(void)
{
std::string test = "test";
ProcessFoo(test); // 编译不通过
return 0;
}
上面的代码编译不通过,因为ProcessFoo 需要的参数是Foo类型,传入的string类型不匹配。如果将Foo构造函数的explicit关键字移除,那么调用ProcessFoo 传入的string就会触发隐式转换,生成一个临时的Foo对象。往往这种隐式转换是让人迷惑的,并且容易隐藏Bug,得到了一个不期望的类型转换。所以对于单参数的构造函数是要求explicit声明。
- 如果不需要拷贝/移动函数,请明确禁止。如果用户不定义,编译器默认会生成拷贝构造函数和拷贝赋值操作符。
class Foo {
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
};
/* 将拷贝构造或者复制操作符设置为private,并且不实现 */
- 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止
// 同时出现
class Foo {
public:
...
Foo(const Foo&);
Foo& operator=(const Foo&);
...
};
// 同时default, C++11支持
class Foo {
public:
Foo(const Foo&) = default;
Foo& operator=(const Foo&) = default;
};
// 同时禁止, C++11可以使用delete
class Foo {
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
};
// 使用delete
class Foo {
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
}
- 禁止在构造函数和析构函数中调用虚函数,会导致未实现多态的行为
class Base {
public:
Base();
virtual void Log() = 0; // 不同的派生类调用不同的日志文件
};
Base::Base() // 基类构造函数
{
Log(); // 调用虚函数Log
}
class Sub : public Base {
public:
virtual void Log();
};
当执行如下语句: Sub sub; 会先执行Sub的构造函数,但首先调用Base的构造函数,由于Base的构造函数调用虚函数Log,此时Log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为。同样的道理也适用于析构函数。
3、继承
- 基类的析构函数应该声明为virtual,只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用:
class Base {
public:
virtual std::string getVersion() = 0;
~Base()
{
std::cout << "~Base" << std::endl;
}
};
class Sub : public Base {
public:
Sub() : numbers(NULL)
{
}
~Sub()
{
delete[] numbers;
std::cout << "~Sub" << std::endl;
}
int Init()
{
const size_t numberCount = 100;
numbers = new (std::nothrow) int[numberCount];
if (numbers == NULL) {
return -1;
}
...
}
std::string getVersion()
{
return std::string("hello!");
}
private:
int* numbers;
};
int main(int argc, char* args[])
{
Base* b = new Sub();
delete b;
return 0;
}
由于基类Base的析构函数没有声明为virtual,当对象被销毁时,只会调用基类的析构函数,不会调用派生类Sub的析构函数,导致内存泄漏。
注意:不管什么时候delete子类,都会调用父类的析构函数。至于析构函数是不是虚函数,主要是保证在delete父类指针,该指针又指向子类对象的时候,能够正确调用子类的析构函数。通过子类对象的指针删除子类对象时,无论父类的析构函数是不是虚的,都会调用父类的析构函数。但是通过父类对象的指针(指向子类对象)删除对象时,如果父类的析构函数不是虚的,那么就不会调用子类的析构函数。所以为了保证正确性,要将会被派生的类的析构函数(是不是就是基类)声明为虚的。
- 禁止虚函数使用缺省参数值,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。
class Base {
public:
virtual void Display(const std::string& text = "Base!")
{
std::cout << text << std::endl;
}
virtual ~Base(){}
};
class Sub : public Base {
public:
virtual void Display(const std::string& text = "Sub!")
{
std::cout << text << std::endl;
}
virtual ~Sub(){}
};
int main()
{
Base* base = new Sub();
Sub* sub = new Sub();
...
base->Display(); // 程序输出结果: Base! 而期望输出:Sub!
sub->Display(); // 程序输出结果: Sub!
delete base;
delete sub;
return 0;
};
- 在重写虚函数时请使用override 关键字,override 关键字保证函数是虚函数,且重写了基类的虚函数。如果子类函数与基类函数原型不一致,则产生编译告警。
class Base {
public:
virtual void Foo();
void Bar();
};
class Derived : public Base {
public:
void Foo() const override; // 编译失败: derived::Foo 和 base::Foo 原型不一致,不是重写
void Foo() override; // 正确: derived::Foo 重写 base::Foo
void Bar() override; // 编译失败: base::Bar 不是虚函数
};
1. 基类首次定义虚函数,使用virtual 关键字
2. 子类重写基类虚函数,使用override 关键字
3. 非虚函数, virtual 和override 都不使用
- 多态的行为必须是在指针或引用的情况下才有效,对值变量来说,一定是静态绑定
class CParent
{
public:
CParent() {}
virtual ~CParent() {}
public:
virtual void Print()
{
std::cout << "1,";
};
};
class CSon : public CParent
{
public:
CSon() {};
virtual ~CSon() {};
public:
void Print()
{
std::cout << "2,";
};
};
void Test1(CParent& oParent)\\传引用
{
oParent.Print();
}
void Test2(CParent oParent)\\传值
{
oParent.Print();
}
main()
{
CSon * p = new CSon();
Test1(*p);//调用子类的print方法
Test2(*p); //调用父类的print方法
delete p;
}
- 如果父类里声明了某函数为虚函数,则在子类此函数的声明里不管有没有"vitrual"关键子,都是虚函数。即使访问权限发生变化,包括析构函数也是这样。
class A
{
public:
virtual void test();
};
class B: public A
{
public:
void test();
...
};
class C: public B
{
public:
void test();
...
};
/* B类的test函数是虚函数,而C类的也是 */
class C: public B
{
private:
void test();
...
};
/* 则test一样是虚函数. */
- 析构函数只需要指定基类为虚函数,则派生类中所有析构函数都为虚函数。
class Base
{
public:
virtual ~Base()
{
std::cout << "base" << std::endl;
}
};
class Derive1:public Base
{
public:
~Derive1()
{
std::cout << "derive1" << std::endl;
}
};
class Derive2:public Derive1
{
public:
~Derive2()
{
std::cout << "Derive2" << std::endl;
}
};
int main(void)
{
Derive1 *a = new Derive2();
delete a;
return 0;
}
输出:
Derive2
derive1
base
- 虽然抽象基类的析构函数可以是纯虚函数,但是实例化其派生类对象,仍必须提供抽象基类中析构函数的函数体。
4、多重继承
在实际开发过程中使用多重继承的场景是比较少的,因为多重继承使用过程中有下面的典型问题:
1. 菱形继承所带来的数据重复,以及名字二义性。因此,C++引入了virtual继承来解决这类问题;
2. 即便不是菱形继承,多个父类之间的名字也可能存在冲突,从而导致的二义性;
3. 如果子类需要扩展或改写多个父类的方法时,造成子类的职责不明,语义混乱;
4. 相对于委托,继承是一种白盒复用,即子类可以访问父类的protected成员, 这会导致更强的耦合。而多重继承,由于耦合了多个父类,相对于单根继承,这会产生更强的耦合关系。
多重继承具有下面的优点: 多重继承提供了一种更简单的组合来实现多种接口或者类的组装与复用。