《Effective C++》1-4 const成员函数、static、确定对象使用前已先被初始化

本文介绍了C++编程中的一些最佳实践,包括用const、enum、inline替换#define,详细讨论了const成员函数、static静态成员的使用,并强调了确定对象在使用前已被初始化的重要性。通过实例解析了const的逻辑常量性和静态成员的特性,以及如何避免非const版本函数与const版本函数的代码重复。

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

条款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)简单的宏定义:

  1. 无参  #define <宏名>  <字符串>         例: #define PI 3.1415926
  2. 带参  #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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值