和我一起读effective C++ ——让自己习惯C++

标签: effective C++


Item1 : 视C++为一个语言联邦

  • C
  • Object-Oriented C++
  • Template C++
  • STL

C++可视为4个次级语言的组合,当你从次级语言中切换,高效编程守则会要求你改变策略。
比如:
在基于C的设计中,内置类型通常pass-by-value比pass-by-reference高效;
而在O-O设计中,由于用户自定义构造函数、析构函数存在,使用 pass-by-reference-const更高效;
运用Template C++时更是如此,此时你甚至不知道所处理对象的类型;
一旦跨入STL,pass-by-value又变得适用,因为STL的迭代器和函数对象是在C指针上塑造的。


Item2 : 尽量以const, enum, inline 替换 #define

#define 将标识符定义为宏,预处理阶段完成文本宏替换。由预处理器完成而不是编译器,这个条款意味着:宁愿以编译器替换预处理器

不利于调试

#define ASPECT_RATIO 1.653
ASPECT_RATIO = 1.5;

老编译器不会记录宏替换信息,如果ASPECT_RATIO编译出错,错误信息提示会是1.653而让你迷惑,当然,现代编译器错误信息会记录宏替换信息,告诉你错误信息关于ASPECT_RATIO

容易出错

#define MAX(a,b) a>b?a:b
3 * MAX(5,6) //结果是5而不是18 --> 3 * 5>6?5:6
#define DIVIDE(a,b) a/b
DIVIDE(3+5,2) //结果是5而不是4 --> 3+5/2

//正确行为
#define MAX2(a, b) ((a) > (b) ? (a) : (b))

然而,除了需要加括号,#define还有别的问题

#define MAX(a, b) (a) > (b) ? (a) : (b)
MAX(i++,j) //i自增次数取决于j,调用者知道吗?

enum hack

当我们需要使用class专属常量,enum hack可以作为static const的替代方案(int)。

class A
{
private:
static const double value;
};
const double A::value = 1.1;

class B
{
private:
enum { value = 5};
int scores[value];
};

此外,取 const 地址合法,而取 enum 地址不合法,取 #define 的地址也不合法,从这点上看,enum 更像 #define

#define TIME 5
enum {TIME2 = 5};
const int TIME3 = 5;
static const int TIME4 = 5;

总结:

  • 对于单纯常量,最好以 const 对象或 enums 替换 #define
  • 对于形似函数的宏,最好用inline函数替换 #define

Item3 : 尽可能的使用const

const 作为一种约束,能区分是否可以改变对象,防止客户错误使用你的代码。

常量的声明

指针的声明:

char greeting[] = "Hello";
char* p = greeting;                 //non-const pointer, non-const data
const char* p = greeting;           //non-const pointer, const data
char* const p = greeting;           //const pointer, non-const data
const char* const p = greeting;     //const pointer, const data

const 出现在 * 的左边则被指向的对象是常量,出现在 * 右边则指针本身是常量。
const 出现在类型的左边和右边则是等效的

void f1(const Widget* pw);
void f2(Widget const * pw);

STL迭代器以指针为根基模塑而来,迭代器的作用就像T* 指针,如果希望迭代器所指data不可变,需要 const_iterator

std::vector<int> vec;
//iter like T* const
const std::vector<int>::iterator iter = vec.begin();    
*iter = 10;     //ok, change what iter point to
++iter;         //error, iter is const

//cIter like const T*
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10;    //error, *cIter is const
++cIter;        //ok, change cIter

对于返回值,声明为 const 也能阻止意外的发生

//对两个有理数的乘积不需要接受赋值
const Rational operator*(const Rational& lhs, const Rational& rhs);

//避免用户使用出现这样的暴力行为,编译器会阻止
Rational a, b, c;
if (a * b = c) {
}

对指针、迭代器、参数、返回值 都可以尽量声明为 const

const 成员函数

const 实施于成员函数,有个明显的好处: 使class接口容易理解,得知哪个函数可以改动对象,哪个不可以。

成员方法添加常量限定符属于函数重载,因此在类外实现需要加 const 关键字。
常量对象只能调用常量方法,非常量对象优先调用非常量方法,如不存在非常量方法会调用同名常量方法。

class TextBlock {
public:
    const char& operator[](std::size_t position) const
    { return text[position]; }
    
    char& operator[](std::size_t position)
    { return text[position]; }
private:
    std::string text;
};

TextBlock tb("hello");
const TextBlock ctb("world");
std::cout << tb[0];         //ok read non-const
std::cout << ctb[0];        //ok read const
tb[0] = 'x';                //ok write non-const
ctb[0] = 'x';               //error wirte const

比特常量(bitwise const):成员函数只有在不更改对象之任何成员变量( staic 除外)时才可以说是 const。也即是说,const 成员函数不能修改非常量数据成员。
比特常量是C++对常量性的定义,然而一个满足比特常量性的方法,表现的却可能不像 const,它确实满足bitwise特性,但是它结果是反直观的:它改变指针所指的值

class TextBlock{
    char* text;
public:
    char& operator[](int pos) const{
        return text[pos];
    }
};

const TextBlock tb;
char *p = &tb[1];
*p = 'a';

这种错误是不该有的,因为创建一个常量对象,而且只对它调用 const 成员函数,却改变了它的值(尽管对象的bit没有改变,但所指的值变了)
这种情况产生了:

  • 对非常量静态数据成员(non-const data)无法修改
  • 对指针成员(non-const data pointer)所指数据却可以修改

于是导出另一种观点,const 成员函数可以修改它所处理的对象内某些bits,但是只有在客户端侦测不出的情况下才可以如此。这观点叫逻辑常量(logical constness)
常量成员函数直接修改数据成员C++编译器不会同意,这时候使用 mutable 释放 non-static 成员变量的bitwise-constness

class CTextBlock {
public:
  std::size_t length() const;
  
private:
  char *pText;

  mutable std::size_t textLength;         // these data members may
  mutable bool lengthIsValid;             // always be modified
};                                     

std::size_t CTextBlock::length() const{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);
    lengthIsValid = true;          
  }
  return textLength;
}

在const和non-const成员函数种避免重复

通常我们会成对的定义常量成员函数和普通成员函数,可能这两个函数仅仅是返回值的不同,这样写两个相同的逻辑并不是我们想要的。const 成员函数并不允许调用 non-const 成员函数,因此只能反之

const char& operator[](size_t pos) const{
    ...
}

char& operator[](size_t pos){
    return const_cast<char&>(
        static_cast<const TextBlock&>(*this)
            [pos]   
    );
}

我们要调用 const 方法,因此需要将普通对象转为 const 对象,当然使用 static_cast<>
返回需要去掉 const 属性,调用 const_cast
注意返回类型是引用,类型转化成什么是需要考虑的


Item4 : 确定对象被使用前已先被初始化

读取未初始化的变量会导致不明确的行为,因此在变量使用前将其初始化是必要的,在构造对象时初始化是一个好习惯。

  • C++的C部分不保证内容初始化 array,而STL部分却能保证std::vector初始化。
  • 了解定义时默认值是有必要的,比如全局变量、局部变量未初始化值。
  • 最佳的处理方法是:在对象使用前初始化,对于内置类型,手动初始化,对于其它,初始化靠构造函数,请在构造函数初始化每一个变量。
  • 构造函数初始化是在初始化列表进行的,而不是构造函数内部进行赋值。
  • const 成员变量、引用类型成员变量初始化在初始化列表进行。
  • 同时,应该知道的是初始化顺序,以免出现意料之外的结果,类的初始化顺序和成员声明顺序一致,而不是初始化列表顺序,而基类又总在派生类前初始化。

最佳实践:以local static 替代 non-local-static

//file1.cc
extern int i = 1;

//file2.cc
int j = i + 1;

实际上无法保证哪个先进行了初始化,方法是写到函数中,以 local static替换

class Ex
{
public:
    static int getI() {
        static int i = 2;
        return i;
    }
private:
};

//file2.cc
int j = Ex::getI() + 1;

这类似单例模式的实现方式:懒汉单例——局部静态变量
注:该方法是线程安全的

class Singleton
{
public:
    ~Singleton() {}
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Singleton& Singleton::getInstance()
    {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() {}
};
  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Effective C++(编程的50个细节) Effective C++(编程的50个细节)着重讲解了编写C++程序应该注意的50个细节问题,书中的每一条准则描述了一个编写出更好的C++的方式,每一个条款的背后都有具体范例支持,书中讲的都是C++的编程技巧和注意事项,很多都是自己平时不太注意但又很重要的内容,绝对经典,作者Scott Meyers是全世界最知名的C++软件开发专家之一。 电子书PDF格式下载:http://www.yanyulin.info/pages/2013/11/effective.html 1、从C转向C++ 条款1:尽量用CONST和INLINE而不用#DEFINE 条款2:尽量用而不用 条款3:尽量用NEW和DELETE而不用MALLOC和FREE 条款4:尽量使用C++风格的注释 2、内存管理 条款5:对应的NEW和DELETE要采用相同的形式 条款6:析构函数里对指针成员调用DELETE 条款7:预先准备好内存不够的情况 条款8:写OPERATOR NEW与OPERATOR DELETE要遵循常规 条款9:避免隐藏标准形式的NEW 条款10:如果写了OPERATOR NEW就要同时写OPERATOR DELETE 条款11:为需要动态分配内存的类声明一个拷贝构造函数和一个赋值函数 条款12:尽量使用初始化而不要在构造函数里赋值 条款13:初始化列表中成员列出顺序和它们在类中的声明顺序相同 条款14:确定基类有虚析构函数 条款15:让OPERATOR=返回*THIS的引用 条款16:在OPERATOR=中对所有数据成员赋值 条款17:在OPERATOR=中检查给自已赋值的情况 3、类和函数:设计与声明 条款18:争取使类的接口完整并且最小 条款19:分清成员函数,非成员函数和友元函数 条款20:避免PUBLIC接口出现数据成员 条款21:尽可能使用CONST 条款22:尽量用传引用而不用传值 条款23:必须返回一个对象时不要试图返回一个引用 条款24:在函数重载与设定参数默认值间慎重选择 条款25:避免对指针与数字类型的重载 条款26:当心潜在的二义性 条款27:如果不想使用隐式生成的函数要显示的禁止它 条款28:划分全局名字空间 4、类和函数:实现 条款29:避免返回内部数据的句柄 条款30:避免这样的成员函数,其返回值是指向成员的非CONST指针或引用 条款31:千万不要返回局部对象的引用,也不要返回函数内部用NEW初始化的指针 条款32:尽可能推迟变量的定义 条款33:明智的使用INLINE 条款34:将文件间的编译依赖性阡至最低 5、继承与面向对象设计 条款35:使公有继承体现是一个的函义 条款36:区分接口继承与实现继承 条款37:绝不要重新定义继承而来的非虚函数 条款38:绝不要重新定义继承而来的缺省参数值 条款39:避免向下转换继承层次 条款40:通过分层来体现有一个和用...来实现 条款41:区分继承和模板 条款42:明智的使用私有继承 条款43:明智的使用多继承 条款44:说你想说的,理解你说的 6、杂项 条款45:弄清C++在幕后为你所写、所调用的函数 条款46:宁可编译与链接时出错,也不要运行时出错 条款47:确保非局部静态对象在使用前被初始化 条款48:重视编译器警告 条款49:熟悉标准库 条款50:提高对C++的认识
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值