01
C++语言编程规范-常量
02
初始化和类型转换
声明、定义与初始化
03
禁止用 memcpy、memset 初始化非 POD 对象
说明:POD 全称是“Plain Old Data”,是 C++ 98 标准(ISO/IEC 14882, first edition, 1998-09-01)中引入的一个概念, POD 类型主要包括 int, char, float,double,enumeration,void,指针等原始类型及其集合类型,不能使用封装和面对对象特性(如用户定义的构造/赋值/析构函数、基类、虚函 数等)。
由于非 POD 类型比如非集合类型的 class 对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。 即使对集合类型的 class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡 memcpy、memset 操作。
示例:×××产品程序异常退出(core dump)。 经过现场环境的模似,程序产生 COREDUMP,其原因是:
在初始化函数内使用
memset(this,0,sizeof(*this))
进行了类的初始化,将类的虚函数表指针被清空,从而导致使用空指针。
解决方案:使用 C++构造函数初始化,不要使用 memset 函数初始化类对象。
04
变量使用时才声明并初始化
说明:变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并同时初始化,非常方便 地避免了此类低级错误。在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:
⚫ 程序难以理解和维护:变量的定义与使用分离。
⚫ 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(比如零)来初始化,这通常是一种浪费,如果变量在被赋于有效值以前使用,还会导致错误。遵循变量作用域最小化原则与就近声明原则,使得代码更容易阅读,方便了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。
示例:
//不好的例子:声明与初始化分离
string name; //声明时未初始化:调用缺省构造函数
//…….
name=”zhangsan”; //再次调用赋值操作符函数;声明与定义在不同的地方,理解相对困难
//好的例子:声明与初始化一体,理解相对容易
string name(”zhangsan”); //调用一次构造函数
05
避免构造函数做复杂的初始化,可以使用“init”函数
说明:正如函数的变量都在函数内部初始化一样,类数据成员最好的初始化场所就是构造函数,数据成员都应该尽量在构造函数中初始化。
以下情况可以使用 init()函数来初始化:
⚫ 需要提供初始化返回信息。
⚫ 数据成员初始化可能抛异常。
⚫ 数据成员初始化失败会造成该类对象初始化失败,引起不确定状态。
⚫ 数据成员初始化依赖 this 指针:构造函数没结束,对象就没有构造出来,构造函数内不能使用 this 成员;
⚫ 数据成员初始化需要调用虚函数。在构造函数和析构函数中调用虚函数,会导致未定义的行为。
示例:数据成员初始化可能抛异常:
class CPPRule
{
public:
CPPRule():size_(0), res (null) {}; //仅进行值初始化
long init(int size)
{
//根据传入的参数初始化size_, 分配资源res
}
private:
int size_;
ResourcePtr* res;
};
//使用方法:
CPPRule a;
a.init(100);
06
初始化列表要严格按照成员声明顺序来初始化它们
说明:编译器会按照数据成员在类定义中声明的顺序进行初始化,而不是按照初始化列表中的顺序,
如果打乱初始化列表的顺序实际上不起作用,但会造成阅读和理解上的混淆;特别是成员变量之间存在依赖关系时可能导致 BUG。
示例:
//不好的例子:初始化顺序与声明顺序不一致
class Employee
{
public:
Employee(const char* firstName, const char* lastName)
: firstName_(firstName), lastName_(lastName)
, email_(firstName_ + "." + lastName_ + "@huawei.com") {};
private:
string email_, firstName_, lastName_;
};
类定义 email_是在 firstName_,lastName_之前声明,它将首先初始化,但使用了未初始化的 firstName_和lastName_,导致错误。在成员声明时,应按照成员相互依赖关系按顺序声明。
07
类型转换
避免使用类型分支来定制行为:类型分支来定制行为容易出错,是企图用 C++编写 C 代码的明显标志。
这是一种很不灵活的技术,要添加新类型时,如果忘记修改所有分支,编译器也不会告知。使用模板和虚函数,让类型自己而不是调用它们的代码来决定行为。
08
使用 C++风格的类型转换,不要使用 C 风格的类型转换
说明:C++的类型转换由于采用关键字,更醒目,更容易查找,编程中强迫程序员多停留思考片刻,谨慎使用强制转换。
C++使用 const_cast, dynamic_cast, static_cast, reinterpret_cast 等新的类型转换,它们允许用户选择适当级别的转换符,而不是像 C 那样全用一个转换符。
dynamic_cast:主要用于下行转换,dynamic_cast 具有类型检查的功能。dynamic_cast 有一定的开销,建议在调测代码中使用。
#include <iostream>
#include<typeinfo>
class Base {
public:
virtual ~Base() {} // 需要虚函数才能启用RTTI
};
class Derived : public Base {
public:
void derived_func() {
std::cout << "Derived function called" << std::endl;
}
};
int main() {
Base* base_ptr = new Derived();
// 使用 dynamic_cast 进行安全的下行转换
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
if (derived_ptr) {
std::cout << "转换成功" << std::endl;
derived_ptr->derived_func(); // 安全调用派生类函数
} else {
std::cout << "转换失败" << std::endl;
}
// 尝试转换不匹配的类型
Derived* another_ptr = dynamic_cast<Derived*>(new Base());
if (another_ptr) {
std::cout << "转换成功" << std::endl;
} else {
std::cout << "转换失败" << std::endl;
}
delete base_ptr;
delete another_ptr;
return 0;
}
对于引用类型的 dynamic_cast:
#include<iostream>
#include <typeinfo>
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
void derived_func() {
std::cout << "Derived function called" << std::endl;
}
};
void process(Base& base) {
try {
Derived& derived = dynamic_cast<Derived&>(base);
std::cout << "转换成功" << std::endl;
derived.derived_func();
} catch (const std::bad_cast& e) {
std::cout << "转换失败: " << e.what() << std::endl;
}
}
int main() {
Derived derived;
Base base;
std::cout << "处理 Derived 对象:" << std::endl;
process(derived); // 转换成功
std::cout << "\n处理 Base 对象:" << std::endl;
process(base); // 转换失败
return 0;
}
static_cast:和 C 风格转换相似可做值的强制转换,或上行转换(把派生类的指针或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。下行转换(把基类的
指针或引用转换成派生类的指针或引用)时,由于没有动态类型检查,所以不安全的,不提倡下行转换。
上行转换(派生类到基类)- 这是 static_cast 最安全的用法之一
class Base {
public:
virtual ~Base() {}
virtual void base_func() { std::cout << "Base function" << std::endl; }
};
class Derived : public Base {
public:
void derived_func() { std::cout << "Derived function" << std::endl; }
};
int main() {
Derived derived;
Base* base_ptr = static_cast<Base*>(&derived); // 上行转换,安全
base_ptr->base_func(); // 调用基类函数
return 0;
}
reinterpret_cast:用于转换不相关的类型。reinterpret_cast 强制编译器将某个类型对象的内存新解释成另一种类型,相关代码可移植不好。建议对 reinterpret_cast<>的用法进行注释,有助于减少维
护者在看到这种转换时的顾虑。
良好的编码实践:添加注释
// 注意:这里使用 reinterpret_cast 是因为我们需要将特定内存区域
// 解释为另一种类型,这是平台相关的操作,不具有可移植性
class MemoryMappedDevice {
public:
MemoryMappedDevice(void* base_addr)
: control(reinterpret_cast<volatile uint32_t*>(base_addr)),
data(reinterpret_cast<volatile uint32_t*>(static_cast<uintptr_t>(base_addr) + 4)) {}
private:
volatile uint32_t* control;
volatile uint32_t* data;
};
const_cast:用于移除对象的 const 属性,使对象变得可修改。
int main() {
const int ci = 10;
const int* ci_ptr = &ci;
// 移除 const 属性
int* i_ptr = const_cast<int*>(ci_ptr);
*i_ptr = 20; // 现在可以修改值
std::cout << "ci: " << ci << std::endl; // 输出可能是 20,也可能还是 10
std::cout << "*i_ptr: " << *i_ptr << std::endl; // 输出 20
return 0;
}
09
extern void Fun(DerivedClass* pd);
void Gun(BaseClass* pb)
{
//不好的例子: C风格强制转换,转换会导致对象布局不一致,编译不报错,运行时可能会崩溃
DerivedClass* pd = (DerivedClass *)pb;
//好的例子: C++风格强制转换,明确知道pb实际指向DerivedClass
DerivedClass* pd = dynamic_cast< DerivedClass *>(pb);
if(pd)
Fun(pd);
}