EFFECTIVE C++ (万字详解)(一)

2023-11-07

前言:

effective C++ 是一本经典之作,其翻译较为贴合原著,但读起来未免有些僵硬而让人摸不着头脑,所以,我会以更为贴近中国人的理解,对此书进行一些阐释。

条款01:把 C++ 看成一个语言联邦

C++由几个重要的次语言构成

C语言:区块,语句,预处理器,数组,指针等等。

:class,封装,继承,多态......(动态绑定等等)

模板:涉及泛型编程,内置数种可供套用的函数或者类。

STL:STL是个模板库,主要涉及容器,算法和迭代器

在不同情况下使用适合的部分,可以使 C++ 实现高效编程

条款02:用const, enum, inline 替换 #define

1、#define 修饰的记号,在预处理的时候,已经全部被替换成了某个数值,如果出错,错误信息可能会提到这个数值,而不会提到这个记号。在纠错方面很花时间,因为其他程序员不知道这个数值代表什么。我们可以用 const 和 enum 解决这个问题。

//enum hack 补偿做法:
enum 枚举量{para1 = value1, para2 = value2,......}
//将一个枚举类型的数值当作 int 类型使用
//和 #define 很像,都不能取地址,但它没有 #define 的缺点

2、#define 不能定义类的常量,因为被 #define 定义的常量可以被全局访问,它不能提供任何封装性。

3、#define 修饰的宏书写繁琐且容易出错,inline 函数可以避免这种情况:
 

#define MY_COMPARE(a, b) f((a) > (b) ? (a) : (b))
//这是一个三目运算符,如果 a > b,则返回 a,否则返回 b
//宏中的每一个实参都必须加上小括号

//调用:
int a = 5, b = 0;
MY_COMPARE(++a, b);//1
MY_COMPARE(++a, b + 10);//2

/*
1式中,++a => a = 6 => 6 > b = 0 => return ++a;
a 的值竟然增加了两次!
*/

//定义 inline:
#define MY_MAX(a, b) (a) > (b) ? (a) : (b)

template<class T>
inline int MY_COMPARE(const T&a, const T&b)
{
	a > b ? a : b;
} 
//inline 将函数调用变成函数本体
//传入的是 ++a 的值

int main()
{
	int a = 2;
	int b = 2;
	MY_COMPARE(++a, b);
	cout << a << endl;
	//此时 a = 3 

	MY_MAX(++a, b);
	cout << a << endl;
    //此时 a = 5
	
	system("pause");
	return 0;
}

条款03:尽可能使用 const

const 允许我们指定一个语义约束,使某个值应该保持不变

1、const 修饰 变量,指针,函数,函数返回值等,可以使程序减少错误,或者更容易检测错误:

        指针常量:int* const p;//指针地址不可变,指针指向值可变

        常量指针:const int* p;//指针指向值不可变,指针地址可变

        常量指针常量:const int* const p;//都不可变

const 修饰迭代器

        iterator 相当于 T* const //指针常量

        const_iterator 相当于 const T* //常量指针

const 修饰函数返回值

const int max(int a, int b)
{
    a > b ? a : b;
}

int c = 6;
max(a,b) = c;
//将 c 的值赋给 max(a, b) 是没有意义的,const 防止这种操作的发生

2、const 修饰成员函数

        如果两个成员函数只是常量性不同(其他相同)则可以发生重载

                const 类对象调用 const 成员函数

                non-const 类对象调用普通成员函数

bitwise:

        const 成员函数不能改变(除 static)成员变量的值,因为常函数里 this 指针指向的值不可改变。同理,const 对象不可以调用 non-const 函数,因为函数有能力更改成员属性的值。

        但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址(地址是 this 指向的值),则不算是 const 函数 ,但能够通过 bitwise 测试。

        使用 mutable 可以消除 non-static 成员变量的 bitwise constness 约束。

class person
{
public:
	
	person(int a)
	{
		m_id = a;
	}
	
	int& func() const
	{
		m_id = 888;
	}
	
	mutable int m_id;
}; 

int main()
{
	const person p(666);
	p.func();
	
	cout << p.m_id << endl;
	
	system("pause");
	return 0;
}

3、当 const 和 non-const 成员函数有实质的等价实现时,利用两次转型,令 non-const 调用 const 可以避免代码重复。

const char& operator[](int pos) const
{
    //...
    //...
    return name[pos];
}

char& operator[](int pos)
{
    return
        const_cast<char&>//移除第一次转型添加的 const
            (
                static_cast<const classname>(*this)[pos]
                //把 classname 类型数据转换为 const classname
                //使得能够调用 const operator[]
            );
}

条款04:确定对象被使用前已经被初始化

1、内置数据类型

int a = 0;
double b = 0;
char* c = "A C-style string";

2、自定义数据类型

使用成员初值列替换有参构造,且次序和class声明次序相同:
 

class person
{
public:
    person(int age, int id, string name)//这是一个有参构造
    {
        this->m_age = age;
        this->m_id = id;
        this->m_name = name;
    }
    
    int m_age;
    int m_id;
    string m_name;
};

//这是成员初始列
class person
{
public:
    person(int age, int id, string name):
		m_age(age),
		m_id(id),
		m_name(name)
	{}
   
    int m_age;
    int m_id;
    string m_name;
};

/*
成员初始列格式:
    classname(parameter1, parameter2,...):
    member1(value1),
    member2(value2),
    ...
    membern(value)
    {}
*/

有参构造是先执行默认构造,再给成员变量赋值。这有一个坏处,const 修饰的量不可以被赋值,但是可以初始化。

使用成员初值列则省去了默认构造部分。成员初值列在不传参的情况下,可以看成默认构造。

int m_size;
char m_array;

//这两个成员变量在初始化的时候是有次序的,m_array 的大小是由 m_size 指定的
//所以必须先初始化 m_size
//实际上,成员初值列中变量的次序可以和声明次序不同
//但为防止这类错误出现,成员初值列的次序应当和声明次序相同

3、使用 local-static 对象替换 non-local-static 对象

函数内 static 对象是 local-static 对象,函数外 static 对象是 non-local-static 对象。

class teacher
{
    ......
    string tname(){...};
};
extern teacher t;
//声明一个 teacher 对象 t, 预留给用户使用
//不定义是因为我们不知道什么时候用它

class student
{
    ......
    student(params)
    {
        string a = t.tname();
        //使用 t 对象
    }
};

这里就出现问题了,teacher 对象必须在 student 对象之前初始化,但是 student 构造函数中使用的是未初始化的 teacher 对象。

解决办法:利用一个函数,定义并初始化本 static 对象,并返回它的引用。

class teacher
{
    ......
    string pname(){...};
};
teacher& teach()
{
    static teacher t;//定义一个 local-static 对象
    return t;
}

class student
{
    ......
    student(params)
    {
        string a = t().tname();
        //使用 t() 函数返回的引用,引用期间,teacher对象被初始化
    }
};
student& stu()
{
    static student s;
    return s;
}

extern 声明的对象对于 teacher 而言是一个 non-local-static 对象, teacher& 函数内声明的对象对于 teacher 而言是一个 local-static 对象。C++保证,local-static 对象在包含它的函数被调用期间(或者说首次遇到这个对象),会被初始化。也就是说,如果用返回引用的函数访问这个对象,就没有调用未初始化对象的困扰。

条款05:了解C++默默编写并调用哪些函数 

在创建类时,如果自己不定义默认构造,拷贝构造(拷贝运算符),析构函数,那么编译器会自动生成这些函数。

//拷贝运算符:
classname& operator=(const classname& cn){......}

拷贝运算符注意事项:

若成员变量中有引用,或者被 const 修饰等等,拷贝运算符不可被调用。

class person
{
    ......
    const int m_age;
    string& m_name;
    ......
}

person p1("lisa", 18);
person p2("luna", 19);
p1 = p2;//error!
//const 值不可以修改,引用的指向不可以修改

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

任何人都应该是天地间独一无二的,很难有理由有两个一摸一样的人。所以对于类中拷贝构造函数,我们应当阻止他们。但若是不声明,编译器也会自动生成拷贝构造函数。

class person
{
private:
    person(const person&);
    person& operator=(const person&);
    //参数是不必要写的,毕竟这个函数不会被实现
public:
    ......
};

编译器自动生成的函数都是 public 函数,所以我们将 public 改为 private,就可以防止对象调用拷贝构造。

注:private 只有成员函数和友元函数可以调用。

同时也产生了一个问题,如何防止拷贝在成员函数或友元函数中被调用?

答案是建立一个父类,在父类中定义 private 拷贝函数,子类( person 等等)继承父类。因为子类不可以调用父类的 private 函数:

class uncopyable
{
private:
    uncopyable(const uncopyable&);
    uncopyable operator=(const uncopyable&);
};

class person{......};

条款07:给多态基类声明 virtual 析构函数

多态把父类当作一个接口,用以处理子类对象:利用父类指针,指向一个在堆区开辟的子类对象。

class person
{
public:
    person();
    ......
    ~person();
};

class teacher: public person{......};

person* p = new teacher(...); 
...
delete p;
//在堆区开辟的数据要手动删除

上述代码是有问题的。

我们知道,在普通类继承里,删除子类对象会先调用子类的析构,再调用父类的析构。但在多态里情况有所不同。我们删除的是父类指针,调用的只是父类的析构函数,子类析构不会被调用,也就是说,子类对象没有被删除,而指针却没了。这是局部销毁,会造成资源泄漏等错误。

幸运的是,我们可以通过虚函数来解决这个问题。

在多态里,虚函数可以让子类重写父类的函数,同时在虚函数表中生成一个指针,找到子类重写函数的地址,从而让我们可以通过父类访问子类重写的函数。

class person
{
public:
    person();
    ......
    virtual ~person();
};

class teacher: public person{......};

person* p = new teacher(...); 
...
delete p;
//删除 p 的时候调用 virtual ~person();
//virtual 找到子类析构函数的地址,导致子类也可以被删除

纯虚函数使得父类更像一个接口,这里不用多说。

注:多态里父类应该至少有一个虚函数(virtual 析构),若不用做多态,则类里不应该有虚函数。

条款08:别让异常逃离析构函数

释义:在析构函数内部处理异常

我们来看以下案例:

class  person
{
public:
    ...
    ~animal(){...};
    //假设执行这个析构函数会抛出一个异常
};

void test()
{
    vector<person> v;//假设容器中存储了几个对象
    ...
    v.clear();//现在清空容器
    ...
    add();//摧毁容器之后,剩余的其他操作
    ...
}

由于对象属于 person 类,在删除的时候会调用析构函数,而 person  的析构函数会抛出一个异常,且未进行异常处理,所以异常会到 test 函数里。 v 中存储了不止一个对象,所以会同时抛出多个异常到test函数里。

在C++程序中,若是同时存在两个异常,则要么结束程序,要么导致不确定行为。结束程序,剩余的操作就无法完成,这对于程序员来说是一个麻烦。

首先介绍一下异常处理的办法

try
{...}
//try 内部写可能产生异常的语句,没有产生异常,则catch语句不执行,产生则一一匹配
//catch 用于捕获并处理异常,和 case 有异曲同工之妙
catch(...)
{
    1、可以使用 abort(); 函数终止程序
    2、可以吞下这个异常,在 catch 内部做一些处理
}

 了解如何处理异常之后,我们就可以实现如条款所说,在析构函数内部处理异常

class employee
{
public:
    ...
    void furlough(); //这是一个放假函数,不放假则抛出一个异常
    ... 
};

class manager
{
public:
    ...
    ~manager()
    {
       try
        {
            ep.furlough();
        }
       //用于确保工人在劳碌后总是可以放假
        catch(...)
        {
            //内部实现
        }
    }
    ...

private:
    employee ep;
};

但我们对此还有一个想法:

假设员工对不让放假的处理是抗议,如果员工很在意这份工作且不想007,显然,在辞职的情况下抗议是没有意义的,因为他们已经失去了这份工作了。所以他们需要在失去工作之前,对不让放假进行处理。

也就是说,我们需要对函数运行期间抛出的异常做出反应,以防止影响到后面的操作。对此我们可以在类里添加一个普通函数

class manager
{
    ...
    void holiday()
    {
        ep.furlough();//如果没有抛出异常,则析构中 if 不会执行
        //如果抛出异常,则异常回滚到上级函数中
        //我们在上级函数中,holiday 函数之后可以定义一个 try catch 语句对异常进行处理
        fangjia = true; 
    }
    ...
     ~manager()//析构函数处理异常作为二重保证
    {
       if(!fangjia)// if fangjia == false
        {
            try
            {
                ep.furlough();
            }
           //用于确保工人在劳碌后总是可以放假
            catch(...)
            {
                //内部实现
            }
        }
    }
    ...

private:
    employee ep;
    bool fangjia;//bool 的默认值依赖编译器,前文有提到过,一定要初始化。令 fangjia = false
};

条款09:绝不在构造和析构函数中使用虚函数

众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 virtual 找到子类析构,再析构父类),所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。

此时,如果父类的构造和析构函数中有 virtual ,则该函数无法找到子类的地址(或者说无视子类,因为子类被销毁/未被初始化),使程序发生不明确的行为。

所以 virtual 函数的调用无法下降至子类,但是子类可以将必要的构造信息向上传递到父类:


class teacher{
public:
	explicit teacher(int score);//父类的构造
	void score_record(const int& score) const;//non-virtual 函数
	......
}; 
teacher::teacher(const int& score)
{
	......
	score_record(score);//构造执行记录分数的操作
}


class student: public teacher{
public:
	student(pare):
		teacher(get_score(para))//将信息传入父类的构造函数,使其记录一个分数
	{......}
	......
private:
	static int get_score(para);//利用一个 static 函数传递分数的值,static 不会传入未初始化的变量
};

条款10:令 operator= 返回一个 reference to *this

释义:让赋值运算符重载版本返回一个自身,以便实现链式编程。

class employee{
public:
	int m_salary;
	
	employee(int a)//有参构造,赋工资初值
	{
		this->m_salary = a;
	}
	
	employee& operator=(const employee& ep)
	{
		this->m_salary = ep.m_salary;
		return *this;
	}
    //返回其本身
};



	employee e1(5000);
	employee e2(50000);
	employee e3(123456);
	
	e1 = e2 = e3;
    //链式编程

条款11:在 operator= 中处理自我赋值

我们来看一段代码:

class person{...};
person p;
p = p;

这是自我赋值,这种操作看起来有点愚蠢,但是并不很难发生。

比如,一个对象可以有很多种别名,客户不经意间让这些别名相等;

或者如之前所说,父类的指针/引用指向子类的对象,也会造成一些自我赋值的问题。

自我赋值往往没有什么意义,还会有不安全性。

class student{...};
class teacher
{
    ...
private:
    student* s;
};

teacher& teacher::operator=(const teacher& teach)
{
    if(s != NULL)
    {
        delete s;
        s = NULL;
    }
    s = new student(*teach.s);
    return *this;//便于链式操作
}

上述代码是不安全的。如果 *this 和 teach 是同一个对象,那么客户在删除 *this 的时候,也把 teach 删除了,s 就会指向一个被删除的对象。这是不允许的。

我们提供三种方法以解决这个问题:

1、证同检测:

teacher& teacher::operator=(const teacher& teach)
{
    if (this == &teach)
        return *this;
    //证同检测
    
    if (s != NULL)
    {
        delete s;
        s = NULL;
    }
    s = new student(*teach.s);
    return *this;//便于链式操作
}

遗憾的是,证同检测可以保证自我赋值的安全性,但是不能保证“异常安全性”。即,如果 new student 抛出异常,则 s 就会指向一个被删除的对象,这是一个有害指针,我们无法删除,甚至无法安全读取它。

2、记住原指针:

teacher& teacher::operator=(const teacher& teach)
{
    student* stu = s;            //记住原指针

    if(s != NULL)
    {
        delete s;
        s = NULL;
    }

    s = new student(*teach.s);   //如果抛出异常,s 也可以找回原来地址
    delete stu;                  //删除指针

    return *this;//便于链式操作
}

3、copy and swap:

void swap(const teacher& teach)
{......}

teacher& teacher::operator=(const teacher& teach)
{
    teacher temp(teach);    //拷贝一个副本
    swap(temp);             //将副本和 *this 交换 
    return *this;//便于链式操作
}

交换操作不要考虑原本指针内容,可以保证赋值安全性,同时也能保证异常安全性。

条款12:复制对象时勿忘其每一个成分

释义:自定义拷贝函数时,要把类变量写全(子类拷贝不要遗漏父类的变量)。

父类变量通常存储在 private 里,子类不能访问父类 private 对象,所以应该调用父类的构造函数:

class animal
{
public:
    animal(const animal& an)
    {......}
    animal& opeartor=(const animal& an)
    {......}
......
private:
    string typename;
};

class cat: public animal
{
public:
    cat(const cat& c);
    cat& operator=(const cat& c);

private:
    string cat_type;
};

cat::cat(const cat& c)
    :cat_type(c.cat_type),
    //为了不遗漏父类变量,调用父类函数
    animal(c)
{}

cat::cat& operator=(const cat& c)
{
    //为了不遗漏父类变量,调用父类函数
    animal::operator=(c);
    this->cat_type = c.cat_type;
    return *this;
}

值得注意的是,上面代码 copy函数和 "="运算符调用的都是和本身一样的函数。究其原因,copy函数是创建一个新的对象,operator= 是对已经初始化的对象进行操作。

我们不能用 copy调用operator=, 以为这相当于用构造函数初始化一个新对象(父类尚未构造好)

同理,也不能用 operator= 调用 copy, 这相当于构造一个已经存在的对象(父类已经存在了)

条款13:以对象管理资源 

所谓资源,就是不再使用它时,将其还给系统。

周所周知,堆区开辟的数据需要程序员手动释放,否则会在程序结束的时候由系统释放。在此前提下,我们来看一段代码:

class employee{...};

void func()
{
    employee* emp = new employee();//动态分配一个对象
    ...
    delete emp;//手动释放,否则emp跑出 func() 函数,造成资源泄露
}

可以预见,如果在 delete 之前,执行了诸如 return, 抛出异常等等,会导致程序跳过 delete ,使 emp 在堆区开辟的对象未被手动释放,造成资源泄露。(删除的是指针,指针指向的数据没有被删除。)

因为在子函数结束时,其中的类对象会发生析构,所以,我们需要建立一个资源管理类,来防止上述情况的发生。

class employee{...};

class manager
{
    ...
private:
public:
    employee* empPrt;
    ...

    manager(employee* emp):
        empPrt(emp)
    {}

    ~manager()
    {
        ...
        delete empPrt;
    }
};


employee* createmp()
{...}//在堆区创建一个对象

void func()
{
    manager m(createmp());
    ...
}

利用 manager 资源管理类创建员工对象,在 func 函数结束的时候,manager析构释放了员工对象总结:建立资源管理类—>管理类存储资源的地址变量—>管理类的构造函数为变量初始化,析构函数手动释放变量在堆区开辟的内存。

条款14:在资源管理类中小心 copying 行为

有的时候,对于资源管理类的 copy 是不必要的,比如管理员工类,每个员工都是单独的个体。但在另一些情况下,copy 也是被程序员需要的。

我们对资源管理类中的 copy 有三种常用的处理方式:

1、禁止 copy 行为:当我们不想某个对象被复制时

class manager
{
private:
    //将copy和operator=设为私有
...
};

2、深拷贝:当我们想要多个这个对象的复件时

所谓深拷贝就是在堆区重新开辟一个地址,存储和这个对象相同的数据

(相信同学们都熟练掌握了深拷贝)

3、深拷贝之后删除原件:当我们只想要一个这个对象时

补充一点:引用计数法

创建一个对象时,其引用数+1,这个对象指向一个新的对象时,其引用数-1,被指向对象引用数+1,当引用数为 0 的时候,delete 这个对象。

条款15:在资源管理类中提供对原始资源的访问

管理类存放的是资源的指针,我们无法从管理类直接得到一个资源对象(只能得到一个指针,通过指针找到对象)。所以我们最好用显式转化或者隐式转换(自动类型转换)来得到一个资源对象:

class employee{...};

class manager
{
    ...
private:
public:
    employee* e;
    ...
    employee get() const  
	{
		return *e;
	}
    //这是显示转化
    
    operator employee() const
    {
        return *e;
    }
    ...
};

manager m(...);
employee emp = m.get();//调用显式

employee m1 = m;
//隐式,manager 对象转换成了 employee 对象 

条款16:成对使用 new 和 delete 时要采取相同形式

众所周知,数组名表示数组第一个地址

string* s = new char[20];
delete s;

所以执行 delete 时,删除的只是第一个对象,后面19个往往不大可能被删除。

对堆区数组的释放可以使用以下方式:

string* s = new char[20];
delete[] s;

总结:

new <=> delete

new [] <=> delete[]

条款17:以独立语句将 newed 对象置入智能指针

首先介绍一下什么是智能指针:

C++提供智能指针来方便客户对资源进行管理,相当于一个资源管理类。常见的智能指针有:

tr1::shared_ptr<>

auto_ptr<>

它是一个格式像容器的变量类型:

//举例:
tr1::shared_ptr<employee> m(createmp());
manager m(createmp());
//这两个效果差不多
//manager 是自定义的一个资源管理类

以上两个智能指针的主要区别在于 copy 行为上:

 tr1::shared_ptr<>在拷贝上允许深拷贝

auto_ptr<>在拷贝上允许拷贝之后删除原件

我们来看一段代码:

int func();//这是一个普通的函数

//创建一个函数,调用智能指针
useemployee(tr1::shared_ptr<employee> (new employee), func());

//tr1::shared_ptr<employee> (new employee)语句的执行顺序:
//先执行 new employee
//再将 new 的地址存放到 shared_ptr 中

C++对于函数参数的运算顺序有很大的弹性,在其他语言中,是先执行tr1::shared_ptr<employee> (new employee),再执行 func()。

但 C++ 不是,func() 函数可能插在 tr1::shared_ptr<employee> (new employee) 中:

//其他语言
new employee
tr1::shared_ptr
func()

//C++
new employee
func()
tr1::shared_ptr

这时候,如果 func() 抛出异常之类,则 new 的地址就无法置入 shared_ptr 中,造成资源泄露。

所以,我们需要一条独立语句将 new 置入 tr1::shared_ptr 中:

tr1::shared_ptr<employee> emp(new employee);//独立语句存放地址

useemployee(emp, func());//调用

条款18:让接口容易被正确使用,不易被误用

1、保证参数一致性:

void print_date(int year, int month, int day)
{......}

print_date(2022, 28, 9);//1
print_date(2022, 9, 28);//2

在这样一个打印时间的函数接口中,我们按照年月日的顺序输出,但是1式却输出年日月。错误的参数传递顺序造成了接口的误用。

解决办法:

class day{...};
class month{...};
class year{...};

void pd(const year& y, const month& m, const day& d){...}

当然,传递某个有返回值的函数也是可以解决的,但这种方法看起来很奇怪。

2、保证接口行为一致性:

内置数据类型(ints, double...)可以进行加减乘除的操作,STL中不同容器也有相同函数(比如size,都是返回其有多少对象),所以,尽量保证用户自定义接口的行为一致性。

3、如果一个接口必须有什么操作,那么在它外面套一个新类型:

employee* createmp();//其创建的堆对象要求用户必须删除

如果用户忘记使用资源管理类,就有错误使用这个接口的可能,所以必须先下手为强,直接将 createmp() 返回一个资源管理对象,比如智能指针share_ptr 等等:

tr1::share_ptr<employee> createmp();

如此就避免了误用的可能性。

4、有些接口可以定制删除器,就像 STL 容器可以自定义排序,比较函数一样

tr1::share_ptr<employee> p(0, my_delete());//error! 0 不是指针

tr1::share_ptr<employee> p(static_cast<employee*>(0), my_delete());//定义一个 null 指针

第一个参数是被管理的指针,第二个是自定义删除器。

条款19:设计 class 犹如设计 type

对于每一个 class 都要精心设计,要考虑其构造析构函数,初始化和赋值,继承,类型转换,运算符重载,值传递等问题。

条款20:宁以 pass-by-reference-to-const 替换 pass-by-value

释义:用 const引用传递替换值传递

值传递是新创建一个对象,将这个对象和原对象相等,如果用在类里面,当类中成员变量数目较少的时候,也许问题不大(在类里值传递先调用构造,再调用析构)。但当类中成员变量数目过大时,每一次值传递就会造成时间浪费。

引用传递是生成一个别名指向这个地址,其本身是个指针,无论原对象有多少个成员变量,都能在一瞬间找到某一个。用上const令其成为常量指针,即“只读”。

class Number
{
public:
    int m_a;
    ...
    int m_n;//有 n 个变量
    ...
};


void print1_num(Number num);
void print1_num(const Number& num);

Number num1;
print1_num(num1);//构造,析构一个Number对象
print2_num(num1);//传地址

此外,值传递在类里还有一个问题:容易造成切割问题。

比如一个子类继续父类,传递子类对象的时候,可能只创建了一个父类的对象,子类部分缺失了:

class base_class
{
    virtual void func() const;
    ...
};

class derived_class
{
    virtual void func() const;
    ...
};

void print_class(base_class b);//这是一个打印函数

derived_class d;
print_class(d);

当我们把 d 传入后,参数 b 被构造成了一个父类对象,调用 virtual 函数的时候不会调用子类函数。但我们传入的是子类对象。

条款21:必须返回对象时,别妄想返回其 reference

class number{
public:
    number(int a);

    const number operator+(const number& n1, const number& n2);    //创建一个新对象,返回它
    //const number& operator+(const number& n1, const number& n2);

private:
    int m_value;
};

number n1(1);//n1 = 1
number n2(2);//n2 = 2

number n3 = n1 + n2;

周所周知,return 返回的是一个浅拷贝副本,返回一个对象是没有问题的,但如果返回一个引用,原对象被销毁之后,引用的指向也被销毁了,也就是引用指空,出错。

我们当然可以用 new 解决这个问题,但是当变量数目多的时候,程序员往往不知道怎么使用 delete:

n3 = n1 + n2 + n4 + ...

或许有人想到创建一个 static 对象,但这也是有问题的,我们每次调用都是对同一个 static 操作:

const number& operator+(...){static number result;...;return result;}

bool operator==(const number& n1, const number& n2);

number n1, n2, n3, n4;

(n1 + n2) == (n3 + n4);//true

上述对result 进行了两次操作,第一次 n1+n2, 第二次 n3+n4 

注:这不同于链式编程,在上述中,我们并不想改变 n1 或者 n2 的值,否则我们直接返回 n1 或者 n2 的引用就可以了。

总结:虽然返回一个对象需要构造,析构等操作而产生一些代价,但是如果我们不想改变已有的值,就最好不要返回一个引用,而是支付这些代价。(这在时间上会多一点,但创建的对象会随运算符的结束而被销毁。这比“未定义行为(返回一个新建对象的引用)”,“资源泄漏”,“结果出错”要好得多了。)

条款22:将成员变量声明为 private

public:所有都能访问

protected:类对象不可以访问

private:只有类成员函数可以访问

private 优点:

1、使成员变量的控制更为精准:

class person
{
public:
    void setage(...);
    void setname(...);
    void setid(...);
private:
    int m_age;
    string m_name;
    int m_id;
};

 用户通过某一函数控制一个私有变量,防止被误用。

2、使类更有封装性:

让我们看看不封装是什么后果:

void func1()
{
    person p;//创建一个 person 对象
    p.m_id;//调用其中一个变量
    
    func2();//这个函数也调用了 p.m_id
}

咋一看好像没什么,但如果 func2() 嵌套一个和自己同类型的函数呢?(可以看成递归)

那么 p.m_id 就被无限调用,当代码出错想要修改的时候,想想你的头发,那真是一个灾难。

而封装起来,修改只要改一小部分代码就可以了。

protected不比private有封装性,因为protected子类也可以实现上述代码的操作。

条款23:宁以 non-member, non-friend 替换 member 函数 

释义:如果一个成员函数调用了其他的成员函数,那么就要用一个非成员函数替换这个成员函数。

根据条款22,对类变量的操作只能通过类成员函数实现(因为它是私有变量),那么如果一个成员函数内部实现是调用其他的成员函数,则一个非成员函数也可以做到这样的效果:

class preson
{
public:
    void func1();
    void func2();

    void func3()
    {
        func1();
        func2();
    }
};

void use_all(const person& p)
{
    p.func1();
    p.func2();
}

 func3() 和 use_all() 的效果是一样的,但这时候我们倾向于选择 use_all 函数,因为func3()作为一个成员函数,其本身也是个可以访问私有变量的函数。use_all() 函数其本身不可以访问私有变量。所以 use_all() 比 func3() 更有封装性。(能够访问私有变量的函数越少越好)

在了解这点之后,我们做一些更深层次的探讨:

我们称 use_func()(func3()的非成员函数版本)为便利函数。假设一个类有多个诸如 func1() 的函数,根据排列组合,也就有很多便利函数。为了让这些便利函数和它的类看上去更像一个整体,我们把便利函数和类放在一个 namespace 中。于是,我们可以更为轻松地拓展这些便利函数——多做一些排列组合。

总结若一个成员函数调用其他成员函数,那么这个成员函数的非成员函数版本比之拥有更多的封装性,和机能扩充性

条款24:若所有参数皆需类型转换,请为此采用 non-member 函数

举例:有理数类和整数的运算

class Rational
{
public:
    Rational(int numerator = 0, int denominator = 1)//分子与分母
    ...
    const Rational operator*(const Rational& right_number) const;
    ...
};

Rational oneEighth(1, 8);
Rational onehalf(1, 2);
Rational result1 = onehalf * oneEighth;
Rational result2 = onehalf * 2;
Rational result3 = 2 * onehalf;//error!

onehalf*2 相当于 onehaf.operator*(2)

首先创建了一个临时对象 const Rational temp(2);

再让两个 Rational 对象运算。

2*onehalf 是 2 调用了operator*。因为 2 是需要被转换的参数,而 2 的位置和 this(调用operator *) 对象的位置是一样的,所以无法将 2 转换成 Rational 类型,也就无法调用 operator* 函数。

解决办法:使用 non-member 函数,让左右参数的地位平等:

const Rational operator*(const Rational& left_number, const Rational& right_number)
{...}

总结:如果所有参数(运算符左边或者右边的参数)都需要类型转换,用 non-member 函数。

条款25:考虑写一个不抛异常的 swap 函数 

周所周知,swap 可以交换两个数的值,标准库的 swap 函数是通过拷贝完成这种运算的。想想,如果是交换两个类对象的值,如果类中变量的个数很少,那么 swap 是有一定效率的,但如果变量个数很多呢?

你一定联想到了之前提过的,引用传递替换值传递。没错,交换两个类对象的地址就可以很有效率地完成大量变量的 swap 操作。不幸的是,标准库的 swap 并无交换对象地址的行为,所以我们需要自己写 swap 函数。

class person{...};
void my_swap(person& p1, person& p2)
{
    swap(p1.ptr, p2.ptr);
}

这个函数无法通过编译,因为类变量是 private,无法通过对象访问。所以要把它变成成员函数。

class person
{
public:
    void my_swap(person& p)
    {
        swap(this->ptr, p.ptr);
    }
    ...
};

如果你觉得 p1.my_swap(p2) 的调用形式太low了,你可以设计一个non-member 函数(如果是在同一个命名空间那就再好不过了),实现swap(p1, p2),这里不做演示。你还可以特化 std 里的 swap 函数:

namespace std
{
    template<>
    void swap<person> (person& p1, person& p2)
    {
        p1.my_swap(p2);
    }
}

值得注意的是,如果你设计的是类模板,而尝试对swap特化,那么会在 std 里发生重载,这是不允许的,因为用户可以特化 std 的模板,但不可以添加新的东西到 std 里。

还有一点:在上面工作全部完成后,如果想使用 swap ,请确定包含一个 using 声明式,一边让 std::swap 可见,然后直接使用 swap。

template<class T>
void do_something(T& a, T& b)
{
    using std::swap;
    ...
    swap(a, b);
    ...
}

 其中过程:

如果T在其命名空间有专属的 swap,则调用,否则调用 std 的swap。

如果在 std 有特化的 swap,则调用,否则调用一般的 swap。(也即是拷贝)

\\这一点虽然看着很奇怪......

总结:

1、当 std::swap 效率不高时,考虑自定义一个成员函数 swap

2、为成员函数提供非成员函数版本

3、类模板不要特化 swap,类特化 swap

\\4、使用 swap 前要写 std::swap,以便在更多的语境下使用

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

EFFECTIVE C++ (万字详解)(一) 的相关文章

  • WPF DataGrid 多选

    我读过几篇关于这个主题的文章 但很多都是来自 VS 或框架的早期版本 我想做的是从 dataGrid 中选择多行并将这些行返回到绑定的可观察集合中 我尝试创建一个属性 类型 并将其添加到可观察集合中 它适用于单个记录 但代码永远不会触发多个
  • BASIC 中的 C 语言中的 PeekInt、PokeInt、Peek、Poke 等效项

    我想知道该命令的等效项是什么Peek and Poke 基本和其他变体 用 C 语言 类似PeekInt PokeInt 整数 涉及内存条的东西 我知道在 C 语言中有很多方法可以做到这一点 我正在尝试将基本程序移植到 C 语言 这只是使用
  • 在模板类中声明模板友元类时出现编译器错误

    我一直在尝试实现我自己的链表类以用于教学目的 我在迭代器声明中指定了 List 类作为友元 但它似乎无法编译 这些是我使用过的 3 个类的接口 Node h define null Node
  • 调用 McAfee 病毒扫描引擎

    我收到客户的请求 要求使用他们服务器上的 McAfee 病毒扫描将病毒扫描集成到应用程序中 我做了一些调查 发现 McScan32 dll 是主要的扫描引擎 它导出各种看起来有用的函数 我还发现提到了 McAfee Scan Engine
  • STL 迭代器:前缀增量更快? [复制]

    这个问题在这里已经有答案了 可能的重复 C 中的预增量比后增量快 正确吗 如果是 为什么呢 https stackoverflow com questions 2020184 preincrement faster than postinc
  • 在 xaml 中编写嵌套类型时出现设计时错误

    我创建了一个用户控件 它接受枚举类型并将该枚举的值分配给该用户控件中的 ComboBox 控件 很简单 我在数据模板中使用此用户控件 当出现嵌套类型时 问题就来了 我使用这个符号来指定 EnumType x Type myNamespace
  • 通过引用传递 [C++]、[Qt]

    我写了这样的东西 class Storage public Storage QString key const int value const void add item QString int private QMap
  • 传递给函数时多维数组的指针类型是什么? [复制]

    这个问题在这里已经有答案了 我在大学课堂上学习了 C 语言和指针 除了多维数组和指针之间的相似性之外 我认为我已经很好地掌握了这个概念 我认为由于所有数组 甚至多维 都存储在连续内存中 因此您可以安全地将其转换为int 假设给定的数组是in
  • 用于 FTP 的文件系统观察器

    我怎样才能实现FileSystemWatcherFTP 位置 在 C 中 这个想法是 每当 FTP 位置添加任何内容时 我都希望将其复制到我的本地计算机 任何想法都会有所帮助 这是我之前问题的后续使用 NET 进行选择性 FTP 下载 ht
  • C++ 多行字符串原始文字[重复]

    这个问题在这里已经有答案了 我们可以像这样定义一个多行字符串 const char text1 part 1 part 2 part 3 part 4 const char text2 part 1 part 2 part 3 part 4
  • WcfSvcHost 的跨域异常

    对于另一个跨域问题 我深表歉意 我一整天都在与这个问题作斗争 现在已经到了沸腾的地步 我有一个 Silverlight 应用程序项目 SLApp1 一个用于托管 Silverlight SLApp1 Web 的 Web 项目和 WCF 项目
  • C# xml序列化必填字段

    我需要将一些字段标记为需要写入 XML 文件 但没有成功 我有一个包含约 30 个属性的配置类 这就是为什么我不能像这样封装所有属性 public string SomeProp get return someProp set if som
  • 为什么 isnormal() 说一个值是正常的,而实际上不是?

    include
  • 有没有办法让 doxygen 自动处理未记录的 C 代码?

    通常它会忽略未记录的 C 文件 但我想测试 Callgraph 功能 例如 您知道在不更改 C 文件的情况下解决此问题的方法吗 设置变量EXTRACT ALL YES在你的 Doxyfile 中
  • 在 WPF 中使用 ReactiveUI 提供长时间运行命令反馈的正确方法

    我有一个 C WPF NET 4 5 应用程序 用户将用它来打开某些文件 然后 应用程序将经历很多动作 读取文件 通过许多插件和解析器传递它 这些文件可能相当大 gt 100MB 因此这可能需要一段时间 我想让用户了解 UI 中发生的情况
  • 对于某些 PDF 文件,LoadIFilter() 返回 -2147467259

    我正在尝试使用 Adob e IFilter 搜索 PDF 文件 我的代码是用 C 编写的 我使用 p invoke 来获取 IFilter 的实例 DllImport query dll SetLastError true CharSet
  • C++ 中的 include 和 using 命名空间

    用于使用cout 我需要指定两者 include
  • C++ 中的参考文献

    我偶尔会在 StackOverflow 上看到代码 询问一些涉及函数的重载歧义 例如 void foo int param 我的问题是 为什么会出现这种情况 或者更确切地说 你什么时候会有 对参考的参考 这与普通的旧参考有何不同 我从未在现
  • 类型或命名空间“MyNamespace”不存在等

    我有通常的类型或命名空间名称不存在错误 除了我引用了程序集 using 语句没有显示为不正确 并且我引用的类是公共的 事实上 我在不同的解决方案中引用并使用相同的程序集来执行相同的操作 并且效果很好 顺便说一句 这是VS2010 有人有什么
  • Mono 应用程序在非阻塞套接字发送时冻结

    我在 debian 9 上的 mono 下运行一个服务器应用程序 大约有 1000 2000 个客户端连接 并且应用程序经常冻结 CPU 使用率达到 100 我执行 kill QUIT pid 来获取线程堆栈转储 但它总是卡在这个位置

随机推荐

  • 浪漫七夕

    迢迢牵牛星 皎皎河汉女 这一句古老的诗句 映照着那悠远的星空 千百年来 牛郎织女的爱情传说代代传承 唤起人们对纯真爱情的向往 岁月流转 这段美丽的传说孕育出七夕节 是一个祈福许愿 象征爱情的节日 更是我们中国人专属的情人节 不论是情侣还是夫
  • 基于SSM框架的员工管理系统

    一 视频展示 https www bilibili com video BV1xM4y1K71m 二 基本介绍 1 关键技术 开发工具 IntelliJ IDEA 数据库 MySQL 5 7 前端技术 Jquery Bootstrap JS
  • 7个Facebook营销技巧:让你的产品轻松出圈!

    据统计 当前Facebook的月活跃用户为29 34亿 是全球最为活跃的社交媒体平台 跨境电商卖家利用好这个平台 可以让自己的产品给更多人看到 如果你还不知道怎么才能用好Facebook做营销推广 那就接着往下看吧 加入Facebook群组
  • jQuery将json字符串转换为数组

    简单的jQuery代码片段将JSON字符串转换为对象数组 然后插入其值的输出 var data JQUERY4U DASHBOARD data widgets data parseJSON data each data function i
  • CVPR冠军方案分享

    近日 全球三大计算机视觉顶级会议之一CVPR如期举行 深兰科技DeepBlueAI团队斩获TinyAction Challenge 低分辨率视频行为识别挑战赛 的冠军 TinyAction Challenge是第六届动作识别国际挑战赛系列竞
  • PC市场逆势复苏之路:创新与多元化引领未来

    市场调研机构Canalys数据显示 今年一季度 中国市场整体PC出货量同比下降24 至890万台 已是连续第五个季度下跌 今年截至618结束 都没有一家主要的PC厂商愿意发战报 PC市场怎样走出寒冬 谈谈你的理解和看法 一 2022年下半年
  • hihoCoder_A+B

    A B Problem 描述 求两个整数A B的和 输入 输入包含多组数据 每组数据包含两个整数A 1 A 100 和B 1 B 100 输出 对于每组数据输出A B的和 样例输入 1 2 3 4 5 6 样例输出 3 7 11 inclu
  • Unity鼠标控制3D物体的移动、旋转、缩放

    一 鼠标控制3D物体移动 1 使用协程 using System Collections using System Collections Generic using UnityEngine public class ControlMove
  • 使用OpenSSL做RSA签名验证 支付宝移动快捷支付 的服务器异步通知

    由于业务需要 我们需要使用支付宝移动快捷支付做收款 支付宝给了我们 移动快捷支付应用集成接入包支付接口 见支付宝包 WS SECURE PAY SDK 支付宝给的服务器demo只有Java C PHP三种 而我们服务器端使用的是C 这其中就
  • 建立ftp文件服务器群,2.1.6FTP文件服务器搭建.docx

    文件服务器 FTP 配置说明 FTP安装及基本配置 FileZillaServer软件安装 FileZilla Server的安装相对简单 一路按照默认安装即可 如图1 1至图1 8所示 图1 1 点击 I Agree 图1 2 点击Nex
  • echarts在vue中使用不报错,但是不显示

    没有设置div标签的宽和高 div class charts div charts width 900px height 500px
  • 一些神奇好用的网站

    文章目录 1 ilovepdf 2 Google Scholar 镜像 3 LetPub 4 Connected Papers 5 Overleaf 1 ilovepdf 网址 https www ilovepdf com 功能 PDF文件
  • Mysql如何定位慢查询(面试题)

    Mysql如何定位慢查询 面试题 相关概念 慢查询分析 慢查询工具定位 Arthas Prometheus Skywalking Mysql慢查询日志 相关概念 分析MySQL语句查询性能的方法除了使用 EXPLAIN 输出执行计划 还可以
  • 宝尊+艺康 面经

    baozun 研发岗 线下专场面试 宣讲之后现场笔试 笔试都是选择题 不难 38道题 30小题基础知识 比较广 8道推理题 数学推理和图形推理 图形难度大 笔试完之后等叫名字就去和面试官谈话 估计是根据笔试成绩 成绩高的先被叫去 每次面试基
  • TVS的典型应用(图文详解)

    TVS瞬态电压抑制二极管 是一种采用半导体工艺制成的单个PN 结或多个PN结集成的高效型电路保护器件 TVS内部芯片为半导体硅材料 具有很高的可靠性 响应速度快 低动态内阻 低钳位电压 电压精度高 击穿电压一般为 5 的偏差 封装多样化 贴
  • oracle字符集总结

    字符集总结author skatetime 2007 12 4 最近公司的数据库要迁移 所以就此机会总结下字符集的知识 以便自己对字符集更全面 更深入的认识 用了 一小天的时间 我是边测试边写 1 什么是oracle字符集 Oracle字符
  • 分布式事务概述

    1 基础概念 1 1 什么是事务 事务可以看做是一次大的活动 它由不同的小活动组成 这些活动要么全部成功 要么全部失败 1 2 本地事务 在计算机系统中 更多的是通过关系型数据库来控制事务 这是利用数据库本身的事务特性来实现的 因此叫数据库
  • 2022年大厂java高频面试题附带答案解析

    本篇分享的面试题内容主要包括 Java SpringMVC Spring Spring Boot Spring Cloud MyBatis ZooKeeper Dubbo Elasticsearch Redis MySQL RabbitMQ
  • 和枚举类相关的Map类——EnumMap

    1 EnumMap类的简介 EnumMap是一个与枚举类一起使用的Map实现 EnumMap中所有key都必须是单个枚举类的枚举值 创建EnumMap时必须显式或隐式指定它对应的枚举类 EnumMap在内部以数组形式保存 所以这种实现形式非
  • EFFECTIVE C++ (万字详解)(一)

    前言 effective C 是一本经典之作 其翻译较为贴合原著 但读起来未免有些僵硬而让人摸不着头脑 所以 我会以更为贴近中国人的理解 对此书进行一些阐释 条款01 把 C 看成一个语言联邦 C 由几个重要的次语言构成 C语言 区块 语句