《Effective C++》 第一章:适应C++的思考方式

本文介绍了C++编程的几个关键方面:将C++视为多种语言的集合,减少宏定义的使用,广泛运用const限定符,并确保所有对象得到适当初始化。通过具体示例,文章详细解释了如何更好地编写C++代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 将c++看成一组语言的集合

从c++创建之初,它的名字就是"面向对象的c语言",可以看出它其实是在c语言的基础上建立起来诸多其他特性。这些特性之间其实并没有什么统一的关联。现在的C++语言可以看作4种子语言的集合

  • c语言。这个不必多说,c++的基本数据类型、指针、数组等等机制都来源于C。很多C的编程技巧也可以在C++中使用。
  • 面向对象的C++。在该子语言下,c++包含了各种机制,包括类、类的各种成员函数、封装、继承、多态、虚函数。
  • 带模板的C++。模板的内容比较新颖,功能非常强大。借由模板的机制可以创造出各种独一无二、非常优美的程序范式。
  • STL。STL尽管只是一个标准库,但他是一个设计的非常棒、而且在C++中非常常用的库。

这四种子语言各自有不同的编程策略,有时候同一种操作在不同的子语言下需要有不同的策略。比如说是传值还是传递引用?在c语言种传值经常有更高的效率,但假如对于对象来说,传递引用经常更为合适。当使用到STL中的函数及迭代器,由于其本质是基于C的指针来实现的,这时候传递值就更有效率。

四种子语言使得C++看起来比较割裂,但是只要单独从各自的特性来思考,就能更容易理解应该如何更漂亮地编程。

2. 少使用宏定义

2.1 宏的局限性

c语言中#define非常好用。但是在C++中几乎没有任何场景应该使用define。
首先,define有诸多问题。

  • 很难debug。define定义的量不会出现在变量表中,debugger无法对其进行追踪。
  • 有时会增加目标代码的量。尤其是定义长字符串,有可能替换之后程序代码中字面量特别多。
  • 无法对宏定义进行封装、继承。(没办法控制宏的命名空间、访问权限,没办法继承)
    对于宏函数,结果更是难以预料。
    比如:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

宏最容易出现的问题是缺少括号。上述代码的实现看起来比较完备,没有缺少括号。
但你永远无法知道宏会怎么被调用

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a is incremented twice
CALL_WITH_MAX(++a, b+10); // a is incremented once

这时发现:宏并不能代替函数。

2.2 比宏定义更好的方法。

2.1.1 对于定义一般的常数,可以使用const直接来定义。

#define b 1.56
const double b = 1.56

2.1.2对于类成员变量

可以使用static const
比如:

class GamePlayer {
private:
static const int NumTurns = 5; // constant declaration
int scores[NumTurns]; // use of constant
...
};

注意static int 实际上是一个声明,c++不允许只声明而不定义。但是对于static const类型,可以不进行定义。
这里的NumTurns可以直接使用,但是不可以访问它的地址。如果需要对他的地址进行操作,则需要定义该变量。

还有一种名为enum hack的实现方式。

class GamePlayer {
private:
enum { NumTurns = 5 }; // “the enum hack” — makes 
// NumTurns a symbolic name for 5
int scores[NumTurns]; // fine
...
};

这种方法的普适性更强。
某些功能不太好的编译器可能不支持static const未经定义直接使用,但是所有编辑器一定可以使用enum.

2.1.3 对于“宏函数”

我不知道“宏函数”这个名字是否合适。它指的是使用define指令来构造的类似函数的效果。

我们可以使用模板+inline函数实现一个速度非常快的函数,而且出现错误的机会相比宏少得多。

template<typename T> // because we don’t
inline void callWithMax(const T& a, const T& b) // know what T is, we
{ // pass by reference-tof(a > b ? a : b); // const — see Item 20
}

3. 尽可能地使用const

3.1 对于一般的常量:

常量可以作为类的成员、函数中的不变量等等。所有的变量,除非必须要变化它的值,否则都应该设置为const。

值得注意的是常量指针指针常量

char greeting[] = "Hello"; 
char *p = greeting; // 指针可变
// 数据可变
const char *p = greeting; // 指针可变
// 数据不可变
char * const p = greeting; // 指针不可变,
// 数据可变
const char * const p = greeting; // 指针不可变
// 数据不可变

* 后面有const,说明指针是常量
*前面有const,说明指针所指内容是常量

STL中的iterator基于指针,它的常量也需要注意一下:

std::vector<int> vec;
...
const std::vector<int>::iterator iter = // 相当于该指针是常量
vec.begin();
*iter = 10; // OK, changes what iter points to
++iter; // error! iter is const
std::vector<int>::const_iterator cIter = // 相当于指针所指内容是常量
*cIter = 10; // error! *cIter is const
++cIter; // fine, changes cIter

3.2 const修饰函数的返回值和参数

书中举了一个重载*运算符的例子, 以说明我们经常忽视使用const,导致了一些不必要的问题。
比如说:a*b=c,这个式子显然不正确,二元运算的结果不应该被赋值。假如*的返回值不是const,则这个式子不会报错。

3.3 const修饰类的成员函数

常函数不允许修改类的属性,普通函数可以修改类的属性。
一个类可能被实例化为变量,也可能被实例化为常量。假如一个类实例化出一个常量对象,那么它只能调用常函数。
本质上来说,允许常量对象调用的函数都应该被设定为const.
假如类中的某些属性,无论是常函数还是普通函数都可以自由修改,那么可以将其声明为mutable。

3.4 const访问与非const访问

const可以形成函数重载。常量对象访问常函数,普通对象访问普通函数。

class TextBlock {
public:
...
const char& operator[](std::size_t position) const
{
... // do bounds checking
... // log access data
... // verify data integrity
return text[position];
}
char& operator[](std::size_t position) 
{
... // do bounds checking
... // log access data
... // verify data integrity
return text[position];
}
private:
 std::string text;
};

但是这种重载会有大量的冗余代码,可以通过const_cast以及static_cast来优化。

class TextBlock {
public:
...
const char& operator[](std::size_t position) const // same as before
{
...
...
...
return text[position];
}
char& operator[](std::size_t position) // now just calls const op[]
{
return
const_cast<char&>( // cast away const on
// op[]’s return type;
static_cast<const TextBlock&>(*this) // add const to *this’s type;
[position] // call const version of op[]
);
}
...
};

4、保证所有对象被初始化

未被初始化的对象可能会有各种难以预料的问题。

4.1. 务必使用初始化列表

使用初始化列表可以解决很多问题:

提高效率

假如直接在构造函数中赋值,则每一个成员对象上执行的操作是:缺省构造函数-> 赋值。而赋值的开销与具体的实现有关,有可能还需要调用拷贝构造函数、operator=
假如在初始化列表中初始化,则每个对象仅仅会调用拷贝构造函数。

对于c++内置对象,使用初始化列表倒是不会提高效率。

清晰明了

帮助程序员清晰地判断所有的成员都已经被正确地初始化。如果在函数体内找可能比较麻烦。
另外,类成员会按声明顺序初始化,你也应该保证初始化列表中各个类成员出现的顺序与类成员声明的顺序一致以避免歧义。同时这也方便核对所有成员是否正确初始化。

为了使得程序清晰,即使类成员不需要通过拷贝构造函数初始化(比如说仅需要使用缺省构造函数、系统内置数据类型),也应该使用初始化列表。

ABEntry::ABEntry()
: theName(), // call theName’s default ctor;
theAddress(), // do the same for theAddress;
thePhones(), // and for thePhones;
numTimesConsulted(0) // but explicitly initialize
{}

功能更多

有些成员只能使用初始化列表,比如说没有缺省构造函数、const成员,他们必须在初始化列表中初始化。

4.2 使用函数来传递跨文件的static变量

多个文件的static变量的初始化顺序无法保证。假如在A文件中存在静态变量static_A,它的初始化用到了B文件的静态变量static_B, 假如直接使用extern来传递变量,无法保证static_A已经被初始化了。

正确的做法是使用一个函数来传递。

class A{
	static int num;
	... ...
	public: A& get_A(){
	static A a;
	return a;
	}

};

调用A::get_A(),返回的对象一定是被初始化之后的。 并且有另一个好处:假如该对象从来不被调用,那么它也永远不会被创建。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值