Effective C++ (一) 让自己习惯C++

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

  • C++是多元的,使用时以方便为主。

C++最早只是简单的C与对象的结合(C with classes),随着语言的发展,C++已经是一个多重范型语言,一个支持:

  • 过程(procedural)
  • 面向对象(object-oriented)
  • 函数式(functional)
  • 泛型(generic)
  • 元编程(metaprogramming)

C++的语言风格:

  • C
  • Object-oriented C++
  • Template C++
  • STL

多重范式语言(multiparadigm programming language)是一个编程术语,指的是能够支持多种语言编程范式的编程语言。编程范式是一种编程的风格或范式。

根据使用最为方便原则,风格之间会混合使用。如在往函数传递一个对象,若该对象是一个内置类型,使用C风格传值即可;若参数是对象,那么Object-oriented C++传递引用的特性就比较合适;有时候我们甚至不确定处理对象的类型,那么则使用Template模板抽象化参数类型;STL在使用过程中,迭代器、函数对象也应用到了C传值风格。总而言之,C++写程序往往不会是单一的范式,一切以方便为主。

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

  • 对于单纯的常量最好以const或enums代替#define
  • 形似函数的宏,最好改用inline函数
  1. 相对于#define,const更好。原因如下:
  • 浮点数而言,目标码可能更小[1]
  • 更容易发现错误(进入记号表)
  • 有作用域范围
  1. 为什么要用enums?
    作者在写这个条款的时候,编译器不都完全支持类内的static变量,如果不支持,

作者是想说其实就是类的静态变量,现在的C++基本不会有不支持static成员变量的了。

  1. 为什么用inline函数代替#define定义的函数?

为了防止降低#define容易忘记小括号导致的错误,除此inline是个函数,遵循访问规则和作用域约束。

条款03 尽可能使用const

const这个东西在C++中很多地方都有用到,作用非常广泛:

  • classes外部修饰global或者namespace作用域的常量
  • 文件、函数和块作用声明static的对象
  • 指针常量、常量指针和引用常量
3.1 const和指向性类型讨论

const出现的位置决定了指向性对象的top-level和low-level属性,top-level表示本身是常量,low-level则指向的对象是常量。指针可选择top-level和low-level中的一个或全选,引用因为初始化绑定后就不能改变,因此可选属性只有一个low-level(top-level已经默认设置)。

在声明中,const可以和函数返回值、参数和成员函数本身起作用,避免一些无意义的情况。const作为返回值目的是防止类似以下的情况:

class Ratioanl{...}
const Rational operator*(const Rational &lhs,const Rational &rhs);
int main()
{
	Rational a,b,c;
	(a*b)=c;//a*b返回值是一个局部变量,进行赋值,整个式子没有意义,用const可以避免,
	if(a*b=c){...}//a*b返回值赋值运算始终为真,可以通过const避免
}
3.2 const成员函数讨论

const成员函数是必要的,接口更加清晰,是提升C++效率的重要手段;const成员函数是保证不更改数据成员的声明;const成员函数分为两大阵营bitwise和logic,后者通过关键字mutable实现bitwise检查豁免,最后作者介绍了常量性移除用于优化、简化代码。

const成员函数很重要,基于两个理由:

  • 接口更加清晰
  • 使操作const对象成为可能

void A::fun(int i)const;尽管const A a实例是一个常量,但是我们仍能对其进行操作,在保证不修改常量实例下进行操作,作者说,改善C++程序效率的根本办法是pass by reference-to-const,如果不能操作const对象,改善无从谈起。作者还提到,const函数和非const函数是一对重载声明:

void fun(int i)const ;
void fun(int i);

const 成员函数的两个阵营:bitwise constness(physical constness)和logic constness,这两个阵营其实争论点在于如何去定义一个const对象,是一个数据成员改变就是nont-const(bitwise constness),还是根据用于具体逻辑,改变一个两个都可以视作const(logic constness)。

默认情况C++将使用bitwise constness进行常量属性检查,但不是所有情况都工作良好,看下面这个例子:

class CTextBlock{
public:
	char &operator[](std::size_t position) const;
	{
		return pText[position];
	}
private:
	char * pText;
}

bitwise constness失效情况。编译器根据bitwise constness默认规则检查后发现下标运算符实现中没有改变数据成员pText的值,编译顺利通过,但是其返回了一个左值char &,如果用户做如下操作:

const CTextBlock cctb("hello");
char *pc=&cctb[0];
*pc="j";

虽然用户本意是创建一个常量的hello,但是暴露出的引用却改变了他的常量属性。

在有些应用,我们希望打破这种bitwise检查:

class A
{
	public:
		void doSomething()const
		{
			m_i=555;//error,bitwise检查失败。
			m_j=564;//ok,logic检查,特别通行证
		} 
	private:
		int m_i=4396; 
		mutable int m_j=567;
}

有时候我们既要const成员函数,也要non-const成员函数,但是其中内容相差不多,代码较为冗余。作者提供了一种解决方法:常量性移除(casting away constness)

class TextBlock
{
public:
	const char& operator[](std::size_t position) const{
		return text[postion];
	}
	char &operator[](std::size_t postion)
	{
		return 
			const_cast<char &>						//强制转换成const char&
			(static_cast<const TextBlock&>(*this)   //强制转换成const,以便复用const char &operator
				[position];
			)
	}
}

这里利用了两次强制转换:

  • 第一次,为*this添加const属性以便调用const成员函数
  • 第二次,为const operator[ ]移除const,还原none-const属性

注意,是在一般函数调用const函数,不是const函数调用一般函数!后者破坏了const的语义。

条款04 确定对象被使用前已先被初始化

4.1 区分初始化和赋值

一个对象要么执行默认初始化,要么执行值初始化。内置类型有初值则执行值初始化;没有初值执行默认初始化,默认初始化行为与作用域相关,要么初始化为0,要么不执行初始化。

默认初始化结果作用域
不初始化非static和全局作用域
初始化为0static和全局作用域

推荐使用类内初值或初始化列表进行初始化,而不是先构造再进行赋值;有些情况不得不采用前者,这是因为以下情况:

  • const成员
  • 引用对象
  • 数据成员未提供默认构造

列表初始化或者类内初值发生时间早于构造函数。另外,列表初始化在某个情况比类内初值更加灵活,例如一个类中定义了多个构造函数,每个构造函数的初值都有所不同,类内初值都是相同的初值,显然不合适。

4.2 注意初始化顺序

初始化列表只说明数据成员的值,并不保证初始化顺序。看看下面这个例子:

class X
{
	int i;
	int j;
public:
	X(int val):j(val),i(j){}
}

正如一开始说到的,列表初始化表示的是j的值是val,i的值是j,谁先谁后并不保证,因此可能出现j尚未被val初始化就被用于i赋值,这样的i值是没有任何意义的。C++ primer建议,如果可能尽量避免使用某些数据成员来初始化其他成员。

C++对于定义于不同编译单元内的non-local-static对象的初始化顺序是不确定的。static对象其生存周期是从构造到程序结束,它可以分为local-static和non-local-static两种,local-static只有一种情况,那就是位于函数作用域中的;其他的,如global、namespace、class和file作用域都是属于non-local-static,static对象只在main()程序结束后调用。当一个non-static对象(以global为例),在一个文件中被初始化,在另一个文件中被使用,C++无法为我们保证static在使用之时被初始化,所以可能出现一些意想不到的现象。为了解决non-local-static对象这种不确定性,我们可以通过定义一个函数返回一个local-static对象的指针或者引用,因为local-static会在第一次调用这个函数进行初始化,而不是在运行前就进行初始化,从而避免了因为调用顺序而导致的问题,这其实是单例模式的一种常见实现,但是在多线程中可能会出现竞争问题,如何解决?通过在单线程启动阶段手动调用所有的reference-returning函数。

[1] https://www.zhihu.com/question/424561003

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值