条款1:视C++为一个语言联邦
C是面向过程的语言,侧重点在于算法和数据结构。
C++是面向对象的语言,侧重点在于抽象出对象模型,使模型和问题契合。
区别:
Class:C++中的类是C没有的,但是C中Struct在C++中也可使用;Struct成员默认访问修饰符是public,而Class默认的是Private。
动态管理内存:C使用malloc/free函数,而C++除此之外还有new/delete关键字。
Overload、Template:C中没有重载和模板。
C++四个层次:
1、C:内置数据类型 pass-by-value比 pass-by-reference 更合适。
2、Objec-Oriented C++:面向对象。构造函数和析构函数。pass-by-reference-const 更合适。
3、Template C++:泛型编程
4、STL:是template程序库,它对容器、迭代器、算法和函数对象有紧密配合和协调。pass-by-value
条款2:尽量以const,enum,inline替换#define
总结:
1、常量,最好以const对象或enum替换#define。
2、简单函数,用inline函数替换#define。
一、宏的弊端
编译器对宏的处理是在预处理阶段进行的,处理方式的核心思想是:简单替换。
(1)简单的宏定义:
- 无参 #define <宏名> <字符串> 例: #define PI 3.1415926
- 带参 #define <宏名> (<参数表>) <宏体> 例:#define MAX(a, b) ((a)>(b) ? (a) : (b))
(2)无法进入编译器
使用预处理定义了圆周率
#define PI 3.1415926
在预处理时, 所有使用PI的地方都将被替换,之后编译器在编译时从未看到过PI。这时如果遇到错误,报错时给出的是301415926,而不是PI,因为PI从未进入到符号表,这将导致错误难以理解。
二、const
上述问题一种解决办法是使用常量替换宏
const double PI=3.1415926
常量const替换#define时,有两点要注意:
1、替换字符串时,要定义成常量指针(指向常量的指针、底层指针)
#define name "xiaoming"
const string name="xiaoming"
2、 专属于class作用域的常量,而#define不重视作用域。
class GamePlayer{
static const int NumTurns=5;
int scores[NumTurns];
};
static静态成员:
1、隐藏:利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。该对象只属于这个类。
2、 保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
int fun(){
static int count = 10; //在第一次进入这个函数的时候,变量a被初始化为10!并接着自减1,以后每次进入该函数,a
return count--; //就不会被再次初始化了,仅进行自减1的操作;在static发明前,要达到同样的功能,则只能使用全局变量:
}
结果:10 9 8 7 6 5 4 3 2 1
三、enum
不想让别人通过一个pointer或referencer指向你的某个整数常量,enum帮助实现这个约束。
四、inline
条款3、尽可能使用const
总结:
- 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)
- 当const 和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
一、基本规则
顶层const(指针常量):char * const p=greeting
底层const(常量指针、指向常量的指针):const char* p=greeting
1、STL迭代器
迭代器的作用就像个int * 指针,声明迭代器为const 就是声明指针为const ,看成是顶层cosnt
const vector<T>::iterator iter;
如果需要迭代器指向常量,类似底层const。
vector<T>::const_iterator iter;
2、const最有效的是面对函数声明时的应用,令函数返回常量值,可以降低因为客户错误而造成的意外。
格式:const Rational operator* (const Rational& lhs,const Rational& rhs)
二、const 成员函数
类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。
声明:const对象只能调用const成员函数,非const对象可以调用所有类型成员函数。对于后者的理解是this 指针可以指向const this。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
void Display1()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void Display2() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 1, 1);
d1.Display1(); //非const对象,调用非const成员函数
d1.Display2(); //非const对象,调用const成员函数
const Date d2(2018, 1, 1);
//d2.Display1(); //const对象,不能调用非const成员函数
d2.Display2(); //const对象,可以调用const成员函数
system("pause");
return 0;
}
使用const成员函数的原因有两个:(1)class接口容易理解,得到哪个函数可以改动对象内容而哪个函数不能改动(2)使操作const对象成为可能。
真实程序中const 对象大多用于pass by pointer-to-const 或 pass by reference-to-const 的传递结果
void print(const TextBlock& ctb)
{
cout<<ctb[0];
}
bitwise constness和logical constness
bitwise constness:字面上的constness,不能更改对象内任何内容。编译器只能在bitwise constness才编译通过。
logical constness:可以更改,但是客户端不能检测出来。
第一种弊端:包含指针时,顶层const,指针不能改变;但是通过指针改变了所指对象,竟然可以通过编译器。
logic constness举例:有以下类 BigArray
,其成员 vector<int> v;
是一个数组数据结构,为了让外部可以访问该数组,此类提供了一个 getItem
接口,除此之外,为了计算外部访问数组的次数,该类还设置了一个计数器 accessCounter
,可以看到用户每次调用 getItem
接口,accessCounter
就会自增,很明显,这里的成员 v
是核心成员,而 accessCounter
是非核心成员,我们希望接口 getItem
不会修改核心成员v,而不考虑非核心成员是否被修改,此时 getItem
所具备的 const
特性就被称为 logic constness。
class BigArray {
vector<int> v;
int accessCounter;
public:
int getItem(int index) const {
accessCounter++;
return v[index];
}
};
但是,上面的代码不会通过编译,因为编译器不会考虑 logic constness ,于是就有了 bitwise constness 这个术语,可以理解为字面上的 constness 属性,编译器只认 bitwise constness。为了解决这种矛盾,可以把 accessCounter
声明为 mutable
的成员,即
mutable int accessCounter;
此时既保持了 logic constness 特性,编译器又可以通过编译。
三、令non-const版本调用const版本可避免代码重复
在一些类中,const成员函数和non-const成员函数功能类似,在这两个函数中都要执行相同的代码,比如一些检查等。
这样很臃肿,解决办法是实现operator的机能一次并使用它两次。令non-const版本调用const版本可避免代码重复。
class CTextBlock{
public:
const char& operator[](std::size_t position)const
{
prepare();//准备
return pText[position];
}
char& operator[](std::size_t position)
{
prepare();//准备
return pText[position];
}
};
解决办法:
class CTextBlock{
public:
const char& operator[](std::size_t position)const
{
prepare();//准备
return pText[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(static_cast<const CTextBlock&>(*this)[position]);
}
};
这样只是在const operator[]中需要详细的写内容,而non-const里只是更改下,调用const operator[]就可以了。
这个更改分为两部分:
1、static_cast<const TextBlock&>(*this) :为*this 添加const ,表明调用的是const operator[]
static_cast强制转移,将non-const更改为cosnt 。
2、const_cast<char&>:从const operator[]的返回值中移除const 。
const_cast移除const
条款4、确定对象使用前已先被初始化
总结:
1、对内置型对象进行手工初始化
2、构造函数使用初始化列表构造函数,避免在构造函数里使用赋值操作。列表初始化列出的成员变量,排列顺序应该和它们在 class中声明次序相同。
3、不同编译单元内定义的non-local static对象”的初始化,以local static对象 替换 non-local static
一、对内置型对象进行手工初始化
int x=0;
const char* text="A C-style string";
二、构造函数使用初始化列表构造函数
class Point{
public:
Point(int x_, int y_):x(x_),y(y_)
{
}
int x,y;
}
三、不同编译单元内定义的non-local static对象”的初始化
现在有至少两个源码文件,每一个里面至少含有一个non-local static对象。如果一个编译单元的non-local static对象初始化使用了另外一个编译单元的non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义在不同编译单元内的non-local static对象”初始化次序无明确规定。
class FileSystem{
public:
std::size_t numDisks()const;
};
extern FileSystem tfs;//定义在global作用域
class Directory{
public:
Directory( params )
{
std::size_t disks=tfs.numDisks();
}
};
Directory tempDir( params);
std::size_t disks=tfs.numDisks();如果tfs初始化晚于tempDir,那么tempDir会使用尚未初始化的tfs。tfs和tempDir是不同的人写的源码,定义在不同的编译单元,无法确定哪一个先初始化。
解决办法:
既然static对象只有一份拷贝,且只初始化一次,很容易想到“单例模式”。使用local static对象,首次使用时初始化,返回其引用即可(local static声明周期是整个程序),以后再使用无需再次初始化。
class FileSystem{};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory{};
public:
Directory::Directory( params )
{
std::size_t disks=tfs().numDisks();
}
};
Directory& tempDir()
{
static Directory td;
return td;
}