Effective C++:1.让自己习惯C++

1.条款01:视C++为一个语言联邦

经过多年发展,C++已经是一个多重范型编程语言:同时支持过程形式、面向对象形式、泛型形式、元编程形式。任何看起来通用的准则都有例外情况。因此我们要把C++看成由多个子语言构成的语言联邦,在每一个子语言的范围内去讨论代码编写的准则。
目前来看,C++总共可以分成四个部分:

  • C:C++内C语言的部分
  • Object-Oriented C++:面向对象设计的部分
  • Template C++: 泛型编程的一部分
  • STL:Template程序库
    总结
  • C++高效编程视情况而变化,具体情况取决于你使用C++的哪一个子语言

2.条款02:尽量以const , enum , inline替换#define

#define 和const、enum、inline最大的区别在于:

  • C++使用预处理器#define
  • C++使用编译器const、enum、inline
    因此,对于#define的宏变量,并不被编译器所知晓,如果这段代码编译报错,将很难定位错误的位置。例如你写下了下面的代码:
#define d 1.6 

那么编译错误的信息可能会提到1.6,但不会提到d,于是你会在寻找这个编译错误上花去大量时间...
总结:

  • 对于单纯的常亮使用const或者enum替换#define
  • 对于形似函数的宏,最好改用inline函数替代#define

3.尽可能使用const

const可以允许你添加一个语义约束,防止用户误用错用你的代码。
下面是一些关于const的易于混淆的概念:

  1. 对于一个指针(引用)变量,可能会有两个const,出现在指针(引用)左边,表示被指向的对象是常量,即被指向对象的值不能更改;出现在指针(引用)右边,表示指针(引用)本身不能变,即指向的对象能更改。
double d1 = 1;
double d2 = 2; 
const double * ptr1 = &d1;   
double * const ptr2 = &d2;   

*d1 = 2;    //错误,被指向的对象不能更改
ptr = &d1;  //错误,指针本身不能更改
  1. 对于星号(引用符号)之前的const有两种写法
const double * ptr1;
double const * ptr2;

这两种写法都有人用,习惯他
3. 对于STL的迭代器使用const,取得的效果是使得迭代器不能指向其他对象

std::vector vec;
std:vector<int>::iterator iter = vec.begin(); //类似于T * const
iter ++                                       //错误,迭代器本身不能更改

std:vector<int>::const_iterator c_iter = vec.begin();  //类似于const T *
c_iter++                                       //正确,迭代器本身可以更改
  1. const成员函数的含义
    const成员函数有两种流派:第一种是bitwise constness,第二种是logic constness。bitwise constness要求const成员函数不能更改除static成员之外的成员,logic constness则允许const成员函数修改其用到的一些内容。
    编译器采用的是bitwise constness。为了实现logic constness,引入了mutable,使得成员总是能被更改(即使是在const函数内)
    总结:
  2. 当non-const的成员函数很多与const成员函数重复时,可以在non-const的函数里调用const函数,避免代码重复
class A{
  std::string& print const(){
    std::cout<<"hello world"<<std::endl;
    return "hello world";
  }
  print(){
    const_cast<std::string &>(static_cast<const A&>(*this)).print(); //static_cast<const A&>把non-const转为const,const_cast把const再把const属性去掉
  }
};

总结

  • 尽量多使用const,可以帮助编译器侦测出错的用法
  • 编译器遵循bitwise constness ,但在写代码时应高遵循logic constness
  • 当const和non-const有着实质等价的实现时,令non-const版本调用const版本可以避免代码重复

4.确定对象被使用前已经被初始化

在C++中初始化是指对定义的变量赋予初值,对于内置类型来说,由编译器负责初始化。对于自定义的类型,则由构造函数进行初始化。不初始化就使用,会出现未定义的结果。举例来说,在你未初始化一个int时,有些平台会初始化为0,有些平台则给一个未知的数字,有些平台直接程序崩溃。因此最好的办法就是:
对所有的类型都初始化
那么具体如何初始化呢?
内置数据类型直接初始化即可
自定义数据类型则利用构造函数,最好使用初始化列表,而不再函数体内赋值,因为C++的初始化发生在函数体代码运行之前,如果你采用了函数体内赋值的形式,则会首先调用default构造函数,再调用拷贝构造函数,开销增大。还有一细节是:编译器根据变量的声名次序进行初始化(而非在初始化列表内的次序),尽管打乱初始化列表内变量的次序是允许的,但为了代码可读性,一般两者的次序要一致
non-local static类型,举例来说就是你在另外一个文件里声明了一个static变量,但是想在本文件中调用这个变量,会出现问题:C++不保证不同编译单元内的non-local static变量的编译次序,这就导致你在使用某个non-local static变量的时候,可能他还没又被初始化(尽管你写了他的初始化代码)
解决办法是采用单例模式。下面看个例子

//文件A内有一个类A
class A{
  void print(){std::cout<<"hello world !"<<std::endl;}
};
extern A a;

//文件B内有一个类B,使用变量a
class B{
  B(){
    a.print();
  }       
};
B b;//这里出现问题,因为并不能保证a已经被初始化

解决办法是单例模式,即将每一个non-local static搬到自己的专属函数内,并让这个专属函数返回reference指向它所包含的对象。(singleton模式)

class A{
  void print(){std::cout<<"hello world !"<<std::endl;}
  A &a(){                   
    static A tmp;                         //1.声明local static
    return tmp;                           //2.返回引用
  }
};

//文件B内有一个类B,使用变量a
class B{
  B(){
    a().print();    
  }
  B &b(){                   
    static B tmp;                         //1.声明local static
    return tmp;                           //2.返回引用
  }
};
b();

这个手法的原理是函数内的local static对象会在“该函数被调用期间首次遇上定义式时被初始化”
总结

  • 为内置类型手动初始化
  • 构造函数最好使用成员初始化列表,而不要在函数体内赋值。并且初始化列表和成员变量的声明次序应该一致。
  • 为免除跨编译单元之初始化次序的问题,请以local static对象替换 non-local static 对象(具体做法是使用单例模式)
posted @   zlj_shell  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示