effective 的老笔记回顾(一)

本文深入探讨了C++编程中的关键技巧,包括使用const和enum替代#define,初始化对象的最佳策略,处理拷贝构造和析构函数,以及如何避免在构造和析构函数中调用虚函数等。通过遵循这些准则,开发者可以编写出更加健壮、易于维护的代码。

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

第一条款

使用const / enum inline 来代替你的#define

如define一个常量数值

#define num 1.333

使用num出错的时候会报错 #$@%@#$%1.33!@#!@意义不明

const float num;//const代替

class A{
    static float num;
};
float A::num = 1.333;//静态变量代替

//enum huck代替
class A{
    enum {num = 5}
    int a[num];
};
//这里枚举成员当做常量使用,无法改变值,亦无法取得其地址

另外,在定义一个函数的时候

如#define (a,b) f( (a) > (b) ? (a) : (b);

引发问题,a/b的累加将取决于a,b的大小,因为如果用(a++,b)的方式调用时,(a++)被视为整体而展开,结果就是a较大的时候,a在与b比较时累加了一次,之后选择了(a)的时候实际又执行了(a++),也就是a累加了两次。

可以使用相应的template来解决这个问题,定义一个template的同意义比较函数就好了,还可以视使用情况加上inline。

 

结论

const / static变量 / enum hack代替define变量

inline template代替define函数

 

条款二

尽量使用const

 

当确定变量是无法改变(不应该改变)的时候就使用const

函数的返回值也尽量使用const来防止对返回的结果进行一个赋值,有时可能只是想要进行比较的时候却少打了一个等号,使用const会让编译器检测到这个情况(毕竟你不能对常量(右值)赋值)

 

const成员函数

mutable来定义一个可以在const成员函数之中可以修改的成员变量(非static)

后置cosnt的成员函数返回的若是成员,则返回值通常也是cosnt的,为了服从后置const不改变成员值得定义。

(事实上如果想要返回一个普通的成员引用是需要用到const_cast的,否则返回的也是一个const成员的引用)

const的对象会(也只能)调用后置const的成员函数,一般的对象则会调用非const版本,前提这两个版本都有。故其实非cosnt成员函数的返回值可能被赋值,不过只在返回左值时发生

另外stataic成员是可以在const成员函数内改变的,毕竟static变量是对象共享的。

class A {
public :
	A() { a = 15; }
	const int& f() const
	{
		cout << "cosnt" << endl;
		b = 200;                        //可以改变static
		return a;
	}
	int & f()
	{
		cout << "non-cosnt" << endl;
		return a;
	}
	const A fa() const
	{
		return *this;
	}
	A fa()
	{
		return *this;
	}
	int a = 10;
	static int b;
};
int A::b = 100;

void main()
{
	A a;
	const A b;
	a.f() = 100;//非const版本
	b.f();
	a.fa() = A();//调用非后置const版本,返回值为对象,故可以改变
}
//输出
//non const
//const

可以选择使用const 成员来实现non-cosnt成员,但是不要反过来,因为这样就违背了const后置的初衷了(不在函数内改变成员)

一个对[ ]运算符的实现

const_cast<return_type&>(

    static_cast<const type&>(*this)  //把当前的非const实例的this指针转换成const类型的

    [position]//然后调用const类型的[ ]运算符

);//最后除去顶层const(返回的是一个指向的常量指针/引用)

对于顶层const 与底层const

自己不可改变则为顶层cosnt,指向的东西不可改变则为底层const

const int *p为底层,int *const p 为顶层。

 

结论

1、const可以帮助编译器去检测一些难以察觉的错误,比如把值赋给了函数的返回值,函数的返回值、参数、成员函数之后、定义变量这些地方都是cosnt使用的位置

2、在const成员函数中其实是可以改变成员值的,比如改变对象中指针指向的值(而不是指针本身),也就像这样

struct s {
	int c = 0;
	int *p = &c;
	void fun()const {
		*p = 10;
	}
};

3、如果non-const与const成员函数本质上都做着一样的事,那就让const版本去实现非cosnt版本。

 

 

 

 

 

条款三

确认对象使用前已经被初始化

1、使用初始化列表来代替赋值初始化,即使使用默认初始化也把这个变量/类的构造函数写在初始化列表中

class A{

    A():name(){}//默认初始化

}

2、non-local static初始化

global(全局对象),namesapce中的对象;classes内、函数内、file作用域内加上了static的对象均是static对象。定义在函数内的static对象称为non local static对象。所有static对象在main()结束的时候释放(调用析构)

 

编译单元:产出单一目标文件的源码、基本上是单一的源文件加上头文件

 

当一个编译单元互相使用了另一个编译单元的non-local static对象处似乎还的时候,无法保证用来初始化的non-local static先被初始化了,所以应该使用local static对象来解决这个问题

class A{}

extern A a;//声明一个全局a

替换为

class A{}

A &getA()

{

    static A a;

    return a;

}

也就是用一个函数来返回函数内作用相同的对象引用,编译器会保证函数内的loacl static对象使用的时候已经初始化。

(c++11后会保证这种对象在多线程下使用安全)

 

结论

1、总是为内置值赋值(没有显示定义默认构造的时候系统执行的默认初始化可不会初始化内置类型)

2、总是使用初始化列表并且顺序与声明一致,与声明一致;而不是在函数体内赋值,这个时候所有成员都先有了一次默认赋值(如果需要的话),当然,如果要用一个成员初始化另一个成员,那就考虑把这个步骤放到函数体内,防止初始化顺序搞错

3、使用local static的函数方式去保证一个static对象在使用的时候必定初始化了

 

 

 

 

条款四

编译器会自动生成默认构造、拷贝构造、拷贝赋值、析构函数等(如果他们都有必要的话)

拥有 const成员/引用 的类不会合成各种拷贝移动构造成员(任何无法拷贝的行为都会导致拷贝赋值等成员不合成)

 

 

 

 

条款五

拒绝拷贝的使用

=delete

旧的做法是使用private来限定拷贝成员的使用

 

 

 

 

 

 

条款六

为基类定义virtual的析构函数,有派生类时才能正确析构

不要继承string等容器,他们的析构函数非virtual

如果想要一个基类是抽象类,但是又不想要让其中的任何成员函数为pure virtual,那么就把析构函数定义成纯虚函数。

但是此时就需要为这个虚函数提供一个“定义”,派生类需要调用它

class A {
public:
	virtual ~A() = 0;//析构函数要放在public下,析构函数为纯虚函数
};
A::~A() {}//提供了定义

class B :A {
};

int main()
{
	B b;//允许创建对象
}

 

 

 

 

条款七

让析构函数noexcept

析构函数之中不应当执行可能抛出错误的操作,如果某种操作无论如何都需要在销毁对象之前执行,定义一个函数来把它交给你的用户去调用,析构函数中在用户没有调用的时候去用try{}catch{}语句把这个操作拦下,并吞下可能抛出的错误,防止析构函数需要抛出让外界来处理的异常。

class database{
	void close()
{
	db.close()
	closed = true;
}//close可能抛出异常,那么用户应当来调用它,如果用户想要得知处理这个异常

	~database() noexcept
	{
		if(!closed)//如果用户没有调用这个操作,则由我们来调用,但是要掉异常
		{
			try{
				db.close()
			}
			catch(exception &e)
			{处理异常并记录}
		}
	}
}
//处理后可以使用abort()终止,或者exit()按序退出

 

 

 

 

 

 

条款八

不要在构造/析构函数里调用虚函数

调用的虚函数是当前执行的构造函数所构造的对象的版本,即即使使用派生类构造一个对象,但在派生类的构造函数调用基类构造函数,并执行基类构造函数的时候,基类的构造函数中调用的虚函数是基类的版本,其实这个时候的this指针指向的基类,也就是说,这个时候类的实际类型是基类,而不是派生类,因为派生类还没有构造好。这个时候若调用的是一个纯虚函数会无法连接。

class A {
public:
	virtual ~A() noexcept
	{
		try {
			fun();//调用了自己的A.fun(),故无法通过连接
		}
		catch(exception e)
		{

		}
	}
	virtual void fun() = 0;//fun为纯虚函数
};

class B : A {
public:
	~B() override {}
	void fun()override {}
};

int main()
{
	B a;//虽然创建的是B类型对象
}

因此,应该把这些操作放进一个派生类的private static函数中去,这样就可以确保不会使用派生类的尚未初始化的成员,也不会被客户误用;然后在初始化值列表中调用一个带参数的基类构造函数来传入相关的数据,再使用基类的相应功能函数。

class Base {
public:
	Base(string info) { fun(info); }       //完成最后需要基类的数据操作
private:
	static void fun(string info) {}
};

class Derived : Base {
public:
	Derived() :Base(createInfo()){}        //派生类中调用含参数的基类构造
private:
	static const string &createInfo()     //把操作放到派生类的函数中
	{
		return "info";
	}
};

结论

不要在构造/析构函数中使用虚函数,因为使用的是当前的版本,动态绑定不会绑定到当前的派生类,而是绑定到正在构造的基类部分。

 

 

 

 

 

条款九

让operator=返回一个reference to *this

(包括其他的+= -= *= /=等赋值运算符)

即返回一个当前对象的引用来保证连锁赋值的可能,这也使这些函数应该被定义成成员函数

 

 

 

条款十

在operator=中处理自我赋值检测

1、使用自我赋值检测

Base & operator=(const Base& b)
{
		if (this == &b)return *this;
}

2、把参数对象中的指针(需要delete释放的对象)复制一次后赋值到类内,再删除原来的指针指向的对象

class Base {
public:
	Base & operator=(const Base& b)
	{
		if (this == &b)return *this;//自我赋值检测
		int *old = num;//记住旧的指针
		num = new int(*b.num);//复制一份参数对象中指针指向的对象
		delete old;//删除旧的指针
		return *this;
	}
	int *num;
};

是否要结合自我赋值检测和手动复制视使用情况而定

3、使用拷贝并交换

即用swap函数实现operator=(swap需要自己先实现,不是使用 swap(*this, other) 这样是死循环),或者说,先构建一个和参数一样的对象,然后把当前对象的值和新构建的这个对象交换。

3、使用拷贝并交换(copy and swap)
class Base {
public:
	Base & operator=(const Base& b)      //较清晰
	{
		Base copy(b);//拷贝对象
		swap(copy);  //交换参数对象与当前对象
		return *this;
	}
	Base & operator=(Base b)             //更高效,但清晰度略逊
	{
		swap(b);       //直接交换对象,把拷贝的工作交给函数参数的构造阶段,这个swap需要自己定义
		return *this;
	}
private:
	int *num;
	void swap(Base &lhs)
	{
		std::swap(num, lhs.num);
	}
};

 

 

 

条款十一

赋值基类与派生类的每个部分

 

不要想着在赋值运算符里调用构造函数、或是反过来。如果两个函数的操作重复过多,那么就把他们包装到一个priavte中然后在两个函数里去调用他。

、综合实战—使用极轴追踪方式绘制信号灯 实战目标:利用对象捕捉追踪和极轴追踪功能创建信号灯图形 技术要点:结合两种追踪方式实现精确绘图,适用于工程制图中需要精确定位的场景 1. 切换至AutoCAD 操作步骤: 启动AutoCAD 2016软件 打开随书光盘中的素材文件 确认工作空间为"草图与注释"模式 2. 绘图设置 1)草图设置对话框 打开方式:通过"工具→绘图设置"菜单命令 功能定位:该对话框包含捕捉、追踪等核心绘图辅助功能设置 2)对象捕捉设置 关键配置: 启用对象捕捉(F3快捷键) 启用对象捕捉追踪(F11快捷键) 勾选端点、中心、圆心、象限点等常用捕捉模式 追踪原理:命令执行时悬停光标可显示追踪矢量,再次悬停可停止追踪 3)极轴追踪设置 参数设置: 启用极轴追踪功能 设置角度增量为45度 确认后退出对话框 3. 绘制信号灯 1)绘制圆形 执行命令:"绘图→圆→圆心、半径"命令 绘制过程: 使用对象捕捉追踪定位矩形中心作为圆心 输入半径值30并按Enter确认 通过象限点捕捉确保圆形位置准确 2)绘制直线 操作要点: 选择"绘图→直线"命令 捕捉矩形上边中点作为起点 捕捉圆的上象限点作为终点 按Enter结束当前直线命令 重复技巧: 按Enter可重复最近使用的直线命令 通过圆心捕捉和极轴追踪绘制放射状直线 最终形成完整的信号灯指示图案 3)完成绘制 验证要点: 检查所有直线是否准确连接圆心和象限点 确认极轴追踪的45度增量是否体现 保存绘图文件(快捷键Ctrl+S)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值