65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 编码实践指南

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.26/5 (36投票s)

2008年5月20日

GPL3

5分钟阅读

viewsIcon

124058

本文档描述了 C++ 编码风格和实践,旨在帮助开发者编写健壮、可靠且易于其他开发者理解和维护的代码。

目录

引言

为了编写其他开发者能够理解的一致性代码,您应该遵循一些编码风格和实践,而不是发明自己的。这些包括命名约定(如何命名变量和函数)、代码和类布局(包括制表符、空格、括号放置)、强制性的 const 正确性等……要理解本文档,您应该了解面向对象编程和 C++ 的基础知识。除了本文档,您还可以参考一些优秀的在线资源。

命名约定

当你开始写代码时遇到的第一个任务是如何命名你的变量、对象和函数。命名约定的重要性不可低估。提供正确的命名将产生自文档化的代码,易于他人理解。除非你想混淆源代码,否则你应该遵循这些指导方针。

使用一个能毫不含糊地描述变量或对象的名称。函数在其名称中应包含一个实现其功能的动词。

Linux 风格(使用下划线分隔单词,名称全小写)

int some_variable;
float bar_weight;
unsigned int users_number;
bool is_engine_started;
double circle_area;
double m_circle_area; //class private variable, prepend m_ prefix

void* psome_void_pointer;

const int USERS_NUMBER;

int i, j, n, m, tmp; //some local loops variables, temporary variables

namespace mynamespace;

vector<int> users;
vector<char *> user_names;

class SomeClass;

int do_work(int time_of_day);
float calculate_radius(const Circle& rcircle);
int start_io_manager();
int open_dvd_player();

Windows 风格(MFC 应用程序,前缀加上 匈牙利 前缀以标识变量类型)

INT nSomeVariable;
FLOAT fBarWeight;
DWORD dwUsersNumber;
BOOL bIsEngineStarted;
DOUBLE dCircleArea;
DOUBLE m_dCircleArea;  //class private variable, prepend m_ prefix

PVOID pSomeVoidPointer;

const INT USERS_NUMBER;

int i, j, n, m, tmp;  //some local loops variables, temporary variables

namespace MyNameSpace;

vector<int> nUsers;
vector<char *> pszUserNames;

class CSomeClass;

INT DoWork(INT nTimeOfDay);
FLOAT CalculateRadius(const& Circle rCircle);
INT StartIOManager();
INT OpenDvdPlayer(); //if abbreviation takes more than
                     // 2 letters do not capitalize the whole word

.NET 平台(匈牙利命名法,前缀已废弃)

int someVariable;
float barWeight;
unsigned int usersNumber;
bool isEngineStarted;
double circleArea;
double circleArea; //refer to a class private variable
                   // in code as this->circleArea

void^ someVoidPointer;

const int UsersNumber;

int i, j, n, m, tmp;  //some local loops variables, temporary variables

namespace MyNameSpace;

array<int> users;
array<String> userNames;

class SomeClass;

int DoWork(int timeOfDay);
float CalculateRadius(const& Circle circle);
int StartIOManager();
int OpenDvdPlayer();  //if abbreviation takes more than 2 letters
                      // do not capitalize the whole word

选项卡

制表符是 8 个空格,因此缩进也是 8 个字符。其主要思想是清晰地定义控制块的开始和结束位置,如果你有较大的缩进,会更容易看到缩进的工作方式。它还有一个额外的好处,当你的函数嵌套太深时会发出警告。

//very bad
void FaceDetector::estimate_motion_percent(const vec2Dc* search_mask)
{
  if (search_mask == 0) { m_motion_amount=-1.0f; }
  else 
    {
     unsigned int motion_pixels=0;
     unsigned int total_pixels=0;
      for (unsigned int y = get_dy(); y < search_mask->height()-get_dy(); y++) 
      {
       for (unsigned int x=get_dx(); x < search_mask->width()-get_dx(); x++) 
       {
           total_pixels++;
           if ((*search_mask)(y,x)==1) motion_pixels++;
       }
      }
    m_motion_amount = float(motion_pixels)/float(total_pixels);
   }
}

//very good
void FaceDetector::estimate_motion_percent(const vec2Dc* search_mask)
{
        if (search_mask == 0)
                m_motion_amount = -1.0f;
        else {
                unsigned int motion_pixels = 0;
                unsigned int total_pixels = 0;
                for (unsigned int y = get_dy(); y < 
                           search_mask->height() - get_dy(); y++) {
                        for (unsigned int x = get_dx(); x < 
                                 search_mask->width() - get_dx(); x++) {
                                total_pixels++;
                                if ((*search_mask)(y, x) == 1)
                                        motion_pixels++;
                        }
                }
                m_motion_amount = float(motion_pixels) / float(total_pixels);
        }
}

空格、花括号和圆括号

对于函数和类定义,将开括号放在下一行是不含糊的。

void some_function(int param)
{
        //function body
}

class SomeClass
{
        //class body
};

但对于 ifsfors、whiles 等,括号和圆括号有几种可能性。

if(some_condition)
{
        //...
}

if( some_condition )
{
        //...
}

if ( some_condition )
{
        //...
}

if (some_condition)
{
        //...
}

if (some_condition) {
        //...
}

首选方式是最后一种,由 K&R 提出,即将开括号放在行的末尾,并将闭括号放在行首。其原因是尽量减少空行数。

数学表达式中的空格应遵循此风格。

float a, b, c, d;
int e, f, g, h;

a = (b + d) / (c * d);

e = f - ((g & h) >> 10);    //good
e =f-( (g&h ) >>10);        //bad

多重包含保护

用多重包含保护包装你的头文件内容,以防止重复包含。

#ifndef Foo_h
#define Foo_h

// ... Foo.h file contents

#endif

类布局

当你声明一个类时,请记住,如果你不提供以下内容的定义:

  • 构造函数
  • 复制构造函数
  • 赋值运算符
  • 取址运算符(const 和 non-const)
  • 析构函数

C++ 将会自动提供。

//declaring that class
class SomeClass
{
};

//you get these functions provided automatically
class SomeClass
{
public:
        SomeClass() { }                     //constructor
        ~SomeClass() { }                    //destructor
        SomeClass(const SomeClass &rhs);    //copy constructor
        SomeClass& operator=(const SomeClass& rhs); //assignment operator
        SomeClass* operator&();             //address-of
        const SomeClass* operator&() const; //operators;
};

如果你不打算提供拷贝构造函数和赋值运算符,并且你的类在构造函数中分配内存并在析构函数中释放,请将拷贝构造函数和赋值运算符声明为私有成员,以避免意外克隆类对象和重复删除同一内存(在原始对象及其副本中)。

publicprotectedprivate 的顺序提供类声明。这样,类的用户就可以立即看到他们需要使用的 public 接口。

#ifndef ClassName_h
#define ClassName_h

class ClassName
{
public:
    ClassName();
    ClassName(const ClassName& classname);
    virtual ~ClassName();

// Operators
    const ClassName& operator=(const ClassName& classname);

// Operations
    int do_work();
    int start_engine();
    int fire_nuke();        

// Access
    inline unsigned int get_users_count() const;        

// Inquiry                
    inline bool is_engine_started() const;

protected:
    //protected functions for descendant classes
        
private:
    //private data and functions
    
    unsigned int m_users_count;
    bool m_is_engine_started;

};

// Inlines
inline unsigned int SomeClass::get_users_count() const
{
    return m_users_count;
}

inline bool SomeClass::is_engine_started() const
{
    return m_is_engine_started;
}

#endif ClassName_h

将内联函数放在头文件中,并在类定义之外,不要弄乱类的主体。

Const 正确性

Const 正确性是指使用 const 关键字显式声明并防止修改不应修改的数据或对象。从一开始就使用它,因为之后修复 const 正确性将是一场噩梦。

你可以为数据或类成员函数声明提供它。

  • const 声明;
  • 成员函数 const
const int max_array_size = 1024;
max_array_size++;    //compilation error

int a;
int & ra1 = &a;
const int& ra2 = &a;
a = 10;
ra1 = 15;    //ok
ra2 = 20;    //compilation error
//the char data pointed by psour will not be modified
int copy(char* pdest, const char* psour)
{
   //copy data from psour to pdest
}

使用 const 关键字声明成员函数,表示该函数是一个“只读”函数,它不会修改调用它的对象。

class Foo
{
    //...
    void set(int x, int val);
    int get(int x) const;
    //...
};

const Foo* pobj1;        //modification of the pobj1 is forbidden
int i = pobj1->get();    //you can only call const member-functions
                         //with const pointer to class object
pobj1->set(0, 10);       //compilation error

然而,将类成员变量声明为 mutable 允许它被 const 函数修改。

class SomeClass
{
public:
    void some_function() const;
private:
    mutable int a;
};

void SomeClass::some_function() const
{
    a++;    //you can modify a in const function
}

你可以防止一个指针在被赋值后再次被重新指向其他位置。这与引用声明的行为类似。

Foo foo1, foo2;
int data1[100];
int data2[100];

Foo* const pfoo;
int* const pdata;

pdata = data1;
pfoo = &foo1;

*pdata = 100;        //you can change the data
pfoo->set(0, 10);    //or class object

pdata = data2;       //compilation error
pfoo = &foo2;        //you can not reassign the pointer

你也可以同时声明一个 const 对象和一个指针。

//neither Foo object can be changed 
//nor pfoo changed to point to another Foo object
const Foo* const pfoo;
const int* const pdata;

最后,如果你提供了 ()[] 运算符,也请提供它们的 const 版本,以便“只读”对象使用。

class SomeClass
{
    ...
    inline int& operator()(int x);      
    inline int operator()(int x) const; 
    inline int& operator[](int x);      
    inline int operator[](int x) const; 
    ...
};

现在,你将能够使用类对象的普通版本和 const 版本。

SomeClass a;
const SomeClass b;

int i = a(0);
int j = b(0);
a(1) = b(2);
b(3) = a(0);    //compilation error, b object can not be modified

函数返回值

函数返回不同类型的值。最典型的一种是指示函数成功或失败的值。这样的值可以表示为错误代码整数(负数 = 失败,0 = 成功)或“成功”布尔值(0 = 失败,非零 = 成功)。

如果函数名是一个动作或祈使句,函数应该返回一个错误代码整数。如果函数名是一个谓词,函数应该返回一个“成功”布尔值。

int start_engine();           //0 = success, negative = error-codes
bool is_engine_started();     //true = success, false = error

杂项

&* 与类型放在一起,而不是与变量放在一起。这使得类型更加清晰。

//good
int& rval;       //&#39;it is&#39; reference int
float* pdata;    //&#39;it is&#39; pointer float

//bad
int &rval;
float *pdata;

将不能为负值的变量定义为 unsigned(例如,unsigned int array_length 而不是 int array_length)。这将有助于避免意外赋值为负值。

引用是一个 const 指针,如 int* const p,一旦赋值,就不能重新赋值。

int var1 = 100;
int var2 = 200;

int& ref = &var1;
ref = &var2;        //avoid it!

要为 Foo** ppfoo 提供一个常量双指针,请使用声明 const Foo* const* ppfoo

在函数定义中始终提供形式参数。

void set_point(int, int);       //ambiguous, what are parameters? avoid it, 
void set_point(int x, int y);   //good.

将指针与零进行比较。

void some_function(char* pname)
{
    //bad 
    if (!pname) {
            //...
    }
    
    //error sometimes
    if (pname == NULL) {
            //...
    }
    
    //good
    if (pname == 0) {
            //...
    }
}

在函数参数中,类对象请通过引用或指针传递。如果你按值传递,将会创建一个类对象的完整副本,如果它占用了几兆字节的存储空间,这将非常糟糕。

//avoid it, pass big_object by reference or pointer
BigObject modify_it(BigObject big_object)
{
    //a local copy of big_object will be created
    ...
    return big_object;
}

//good practice
void modify_it(BigObject& big_object)
{
    //no copy is created, you can modify the object as usually
}

面向对象编程技巧

切勿返回指向类私有数据成员的指针或非 const 引用。不要破坏封装原则。

class SomeClass
{
public:
    SomeObject* get_object() { return &m_object; }
    //do not break incapsulation principle
    //m_object can be modified outside the class

    const SomeObject& get_object() { return m_object; }
    //good, the m_object will not be
    //inadvertantly modified outside the class        
private:
    SomeObject* m_object;
};

始终在公共类中声明虚析构函数。如果你从没有虚析构函数的父类派生类,那么当您将派生类对象指针转换回父类指针,稍后删除父类指针时,只会调用父析构函数。

class Base
{
    ...
    ~Base();    //should be virtual ~Base()
    ... 
};

class Derived : Base
{
     ...
};

...
Derived* pderived = new Derived();
Base* pbase = (Base *)pderived;
...

delete pbase;    //only ~Base() will be invoked
...

使用友元。如果你不打算让库的用户使用某个类,但你又从其他公共类访问该类的私有成员,那么为其提供公共访问器方法是多余的。将你的类声明为它的友元,这样你就可以访问它的私有数据了。

class MainClass
{
    ...
    void some_function();
    ...
    HelperClass* phelper_class;
    ...
};

void some_function()
{
    ...
    int i = phelper_class->m_variable;    //you gain access to private members 
                                          //of HelperClass from MainClass
    phelper_class->m_variable = 10;
    ...
}

class HelperClass
{
    friend class MainClass;
    ...
private:
    int m_variable;
    ...
};
© . All rights reserved.