本文主要是介绍Effective C++ 摘记(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
(一)、让自己习惯C++
一、C++语言联邦
多重范型编程语言:过程式、面向对象式、函数式编程、泛型编程、模板元编程。
二、const、enum、inline替换#define
const:代替宏变量有助于编译器理解;可以实现类内的常量定义(宏定义对全局有效,不具有封装性)
enum:enum hack,更像define,不消耗内存,无法取地址;
inline:宏函数尽量用inline代替。
三、const
如果const出现在 *左边,则表示数据是常量;如果const出现在 * 右边,则表示指针是常量;如果* 左边和右边都有const,则表示数据和指针都是常量。
const返回值:避免(a*b)=c的错误;
const func(); //函数的返回值不可被改变
const参数:传递指向常量的引用;
const成员函数:允许const属性的重载。
在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立。
const修饰的函数可以被non-const对象调用,而 非const修饰的函数不可以被 const 对象调用。
如果函数返回值是个引用,则改变该值有意义;而如果函数返回值是个 内置数据值,则改变该值无意义,或者不合法。
char& operator[] (int pos) ;改变返回值 char& 合法。
char operator[] (int pos) ;改变返回值 char 无意义(甚至不合法)。
四、对象使用前初始化
构造函数成员初始化列表;
c++规定,对象成员的初始化发生在进入构造函数本体之前。
static变量,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象,classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象成为non-local static对象,在程序结束时static对象会被自动销毁,也就是他们的析构函数会在main()结束时被调用
local-static变量是在函数内部定义的static变量,局部静态变量和全局静态变量都存储在静态内存中,所以函数退出后局部静态变量不会被释放;不同于全局静态变量的是,它不是file scope visible的,也就是说在file scope 范围内是不能访问的,只能是再次调用该函数时该变量可见(注意static变量只初始化一次)。
c++对于定义在不同编译单元内的non-local-static变量的初始化没有明确定义。
//a1.cpp
#include <fstream>
#include <iostream>
#include "a3.cpp"
using namespace std;
Write a;
int main()
{ system("pause"); return 0;
}
//a2.cpp
#include <fstream>
using namespace std;
extern ofstream out("a6.txt");
//a3.cpp
#include <fstream>
using namespace std;
extern ofstream out;
class Write
{
public: Write() { out<<"a4.txt"; } };
a2.cpp中,定义了全局变量 out,a3.cpp中定义了一个类 Write,它的构造函数初始化依赖于 out,因为2个文件的便宜顺序是不确定的,所以,很有可能 当Write()调用的时候,out全局变量并没有被初始化,造成程序错误。
为了解决这个问题,可以将每个non-local-static对象搬到自己的专属函数内,即成为 local-static对象,然后函数返回该local-static对象的引用,然后用户调用这些函数,而不直接指涉这些对象。可以这么做是因为:c++保证,函数内的local-static对象会在函数被调用期间,首次遇上该对象定义式时被初始化,所以保证外部使用的static对象是经过初始化的。
使用时调用,单例模式,多线程不安全。
(二)、构造/析构/赋值运算
五、C++默认编写的函数
默认构造、复制构造、析构、赋值运算符。
六、拒绝自动生成的函数
拒绝编译器自动生成拷贝构造函数和重载赋值运算符, 可以:
(1)私有化拷贝构造和赋值运算符;
class Test
{
public:
Test();
private:
Test(const Test&);//只声明,不进行实现。
Test & operator=(const Test&);
}
(2)通过私有继承UnCopyable手工类。
class Test: private UnCopyable //通过继承 UnCopyable类,当编译器想要为Test类自动生成代码时,会调用Uncopyable类的相应函数,触发错误。
{
}
七、多态基类声明虚析构函数
(不)具有多态性质基类(不)需要虚析构函数;
任何class只要带有virtual成员函数,就需要带有一个virtual 析构函数。
如果通过多态的方式将一个基类的指针指向一个子类的对象(该对象位于堆内存中),如果想要delete该子类对象,通过delete 指向该对象的基类指针来操作。如果基类的析构函数为 virtual,则可以完美的删除子类对象,否则会出现局部删除的现象(只删除基类的内容),造成内存泄露。
class 设计的目的如果不是作为base class使用,或者不具备多态性,就不该声明为virtual。
八、不让异常逃出析构
(1)析构函数绝对不要吐出异常,如果一个析构函数调用的函数可能发生异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或者结束程序
(2)如果客户需要对某个函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
九、不在构造和析构中调用虚函数
调用后仅仅是自身的虚函数,而非子类(构造是先构造父类,此时子类的信息还不知道;析构先析构子类,当析构父类时,子类的信息已经清除)。
需要子类构造信息解决方案:子类使用静态函数构造基类的参数。
使用辅助函数 static std::string createLogString(parameter)创建一个值给base class 比在成员初始值列列出所有的实参,更加方便。注意辅助函数应该为静态函数,使得它不可能意外指向“初期未成熟的子类对象中的尚未初始化的成员变量”。(静态成员函数内部不能使用非静态成员变量)
十、operator=返回*this的引用
允许连续赋值。
int x = y = z = 15; 其处理过称为 x = (y = (z = 15)) , 其中,= 赋值操作符返回的是 =(赋值操作符)左侧参数的引用,即 z = 15 返回 z 的引用。整个式子相当于, z = 15, y = z, x = y.
为了实现连锁赋值,必须使得赋值操作符返回一个reference指向操作符的左侧实参。这是为class实现赋值操作的协议:
这个协议不仅适用于 = 赋值操作符,也适用于所有与赋值相关的操作符。如 +=, -=等。
十一、operator=处理自我赋值
注意资源的释放顺序。
十二、复制对象要面面俱到
不要丢失基类的成员的复制。
Copying函数应该确保复制“对象中所有成员变量”以及“所有 base class成分”
拷贝构造函数是构造一个新的对象,而赋值运算符是对一个已经存在的对象进行操作。所以在拷贝构造函数中避免使用赋值运算符,在赋值运算符的重载函数中避免使用拷贝构造函数。
不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放进第三个函数中,并由两个copying函数调用。
(三)、资源管理
十三、对象管理资源
构造函数获得资源,析构函数释放资源;
以对象管理资源的两个想法:
(1)获得资源后立刻放进管理对象
(2)管理对象利用析构函数确保资源被释放
使用智能指针封装:tr1::shared_ptr和auto_ptr。 auto_ptr<类型> p1, tr1::shared_ptr<类型> p2, 其中p1, p2 为指针类型,不需要加 *。
使用 auto_ptr需要注意不能让多个auto_ptr指向同一个对象(防止重复删除,内存破坏)。为了预防这个问题,auto_ptrs保证若通过拷贝构造函数或赋值运算符来复制他们,他们会变成null,而所复制的指针将获得资源的唯一拥有权。
auto_ptr的替代方案是使用“引用计数型智慧指针” RCSP, RCSP持续跟踪有多少个对象指向某笔资源,并在无人指向该资源时,自动删除该资源。
1、为防止资源泄露,使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源
2、两个常用的RAII classes类是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若采用 auto_ptr, 复制行为会使得它指向null。
十四、资源管理中小心copying
class Lock
{
public:explicit Lock(Mutex* pm):mutexPtr(pm){lock(mutexPtr);};~Lock(){unlock(mutexPtr);};
private:Mutex* mutexPtr;
};//客户对lock的用法符合RAII原则
Mutex m; //定义所需要的互斥器
....
{ //建立区块用于定义关键区Lock m1(&m); //锁定互斥器.... //执行关键区内的操作
} //在区块最末尾,自动解除互斥器锁定
此时,若对lock对象进行复制,Lock m2(m1) 则:
互斥锁加解锁的对象禁止复制;
引用计数法,tr1::shared_ptr<Mutex> mutex_ptr(pm,unlock),含有删除器的指针。
十五、资源管理类提供原始资源访问
原始资源获取;
显式转换—— tr1::shared_ptr和auto_ptr中都提供了一个get成员函数,来执行显式转换,也即它会返回智能指针内部的原始指针(的复件)。
int days = daysHeld(pInv.get());
隐式转换——类型转换函数,使用-> 或者* 操作符进行指针的操作,达到隐式转换。
显式转换比较安全。隐式转换对客户比较方便。
十六、new-delete同型成对
[]的出现与否要对应起来,即使使用了typedef重命名了数组类型。
十七、独立成句的new对象放入智能指针
将new对象转换为智能指针作为参数,可能会被编译器结合其他参数调整顺序,造成内存泄漏。
可以在使用时,分条写代码,使顺序固定。
这篇关于Effective C++ 摘记(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!