C++由几个重要的次语言构成:
C语言:区块,语句,预处理器,数组,指针等等。
类:class,封装,继承,多态…(动态绑定等等)
模板:涉及泛型编程,内置数种可供套用的函数或者类。
STL:STL是个模板库,主要涉及容器,算法和迭代器
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;
}
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 成员函数
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[]
);
}
1、内置数据类型:必须初始化,如下列所示:
int a = 0;
double b = 0;
char* c = "A C-style string";
对于内置类型以外的任何其他东西,初始化责任落在构造函数身上。规则就是每个构造函数都将对象的每一个成员初始化。
2、赋值与初始化区别:
c++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。
对象的初始化是交给构造函数处理的;
1.其中构造函数可使用参数列表进行初始化,发生于默认构造函数被自动调用之时;
2.且C++的成员初始化次序是相同的,按其声明的顺序被初始化;
class test{
private:
int m_id;
int m_num;
float m_score;
public:
test(int id, int num, float score) : m_id(id), m_num(num), m_score(score) {};
};
int main() {
return 0;
}
注意:
1.若出现遗漏的成员变量,可将其改用赋值操作封装在单独的函数内部,供其他函数调用;
2.为内置型对象进行手工初始化;
3.构造函数最好使用成员初始化列表,效率会略微提高,且次序需和声明时一致;
4.为免除跨编译单元的问题,使用local-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 对象在包含它的函数被调用期间(或者说首次遇到这个对象),会被初始化。也就是说,如果用返回引用的函数访问这个对象,就没有调用未初始化对象的困扰。
将需要先初始化的对象封装在专属的函数内部,切将其声明为static,并返回一个reference;
class A{};
A& getA() {
static A a;
return a;
}
当处于多线程时,该方法并不安全,需要在程序的单线程启动阶段手动调用所有reference-returning函数,以此来消除初始化有关的竞速形势;
在创建类时,如果自己不定义默认构造,拷贝构造(拷贝运算符),析构函数,那么编译器会自动生成这些函数
//拷贝运算符:
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 值不可以修改,引用的指向不可以修改
对于类中拷贝构造函数,我们应当阻止他们。但若是不声明,编译器也会自动生成拷贝构造函数。
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{......};
多态把父类当作一个接口,用以处理子类对象:利用父类指针,指向一个在堆区开辟的子类对象
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 析构),若不用做多态,则类里不应该有虚函数。
1、在C++中,不鼓励在析构函数中吐出异常;
由于析构函数正常是用来释放内存的,若在释放内存的同时出现了异常,则后面将会导致内存泄漏;
2、针对于上述问题有一个较佳的策略重新设计接口:
提供一个close函数对原析构函数内容进行实现,在将该接口在析构中调用;
这样处理,可以将异常排除在析构函数之外,否则将会给程序带来过早结束或者发生不明确行为;
class DBConn{
public:
void close(){
db.close();
closed = true;
}
~DBConn() {
if (!closed) {
try{
close();
}
catch{
//退出程序或记录
}
}
}
private:
DBConnection db;
bool closed;
};
建议:
析构函数中绝对不要吐出异常,析构函数应该捕捉异常,将异常吞下或者结束程序;
若该class在运行期间抛出异常,则应该提供一个普通函数,来执行该操作;
众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 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 不会传入未初始化的变量
};
释义:让赋值运算符重载版本返回一个自身,以便实现链式编程。
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;
//链式编程
先来看一段代码:
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;//便于链式操作
}
释义:自定义拷贝函数时,要把类变量写全(子类拷贝不要遗漏父类的变量)。
父类变量通常存储在 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, 这相当于构造一个已经存在的对象(父类已经存在了)
为了确保对象返回的资源总是被释放:
1.把资源放进对象内,我们便可依赖C++的析构函数自动调用机制确保资源被释放;
2.许多资源被动态分配于heap中内置于单一区块或函数内,应在控制流离开时被释放;
3.故标准程序库提供智能指针,可利用其析构函数自动让该对象调用delete;
auto_ptr:
被销毁时,自动删除所指之物;
不能让多个auto_ptr指向同一个对象,使用拷贝构造和赋值操作符会变成nullptr;
shared_ptr:
获得资源后立刻放进管理对象;
可以指向同一个对象;
一般不用于数组;
管理对象运用析构函数确保资源被释放;
所谓资源,就是不再使用它时,将其还给系统。
周所周知,堆区开辟的数据需要程序员手动释放,否则会在程序结束的时候由系统释放。在此前提下,我们来看一段代码:
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析构释放了员工对象总结:建立资源管理类—>管理类存储资源的地址变量—>管理类的构造函数为变量初始化,析构函数手动释放变量在堆区开辟的内存。
当一个RAII(资源取得时机即初始化时机)对象被复制时,一般选择以下可能:
禁止复制;
当该动作对对象不合理时,应禁止(使用uncopyable);
对底层资源祭出引用计数法;
为了保证最后一个对象被销毁,在复制时,应递增引用计数;
复制底部资源;
当复制底部资源时,需要使用深度拷贝,可避免该资源在释放的时候出现问题;
转移底部资源的拥有权;
/**
1、继承uncopyable禁止copy
2、使用shared_ptr删除器
3、不需要声明虚构函数
*/
class Lock : private Uncopyable {
public:
explicit Lock(Mutex* mutex) : m_muetx(mutex) {
lock(m_muetx.get());
}
private:
std::tr1::shared_ptr<Mutex> m_mutex;
};
建议:
复制RAII对象必须一并复制它所管理的资源,故资源的coping行为决定RAII对象的coping行为;
普遍的RAII对象的复制行为:抑制copying、施行引用计数法;
管理类存放的是资源的指针,我们无法从管理类直接得到一个资源对象(只能得到一个指针,通过指针找到对象)。所以我们最好用显式转化或者隐式转换(自动类型转换)来得到一个资源对象:
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 对象
当使用new时会发生两件事:
内存被分配;
构造函数被调用;
当使用delete时也会发生两件事:
析构函数被调用;
delete函数被调用;
new一个数组于单一对象的区别:
1.由于单一对象的内存布局于数组的内存布局不同,故释放的用法也不同;
2.数组使用delete [];
建议使用:若在new中使用[],则在delete中也需要使用[];
首先介绍一下什么是智能指针:
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 (new employee)语句的执行顺序:
//先执行 new employee
//再将 new 的地址存放到 shared_ptr 中
C++对于函数参数的运算顺序有很大的弹性,在其他语言中,是先执行tr1::shared_ptr (new employee),再执行 func()。
但 C++ 不是,func() 函数可能插在 tr1::shared_ptr (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());//调用
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 指针
第一个参数是被管理的指针,第二个是自定义删除器。
对于每一个 class 都要精心设计,要考虑其构造析构函数,初始化和赋值,继承,类型转换,运算符重载,值传递等问题。
释义:用 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 函数的时候不会调用子类函数。但我们传入的是子类对象。
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 的引用就可以了。
总结:虽然返回一个对象需要构造,析构等操作而产生一些代价,但是如果我们不想改变已有的值,就最好不要返回一个引用,而是支付这些代价。(这在时间上会多一点,但创建的对象会随运算符的结束而被销毁。这比“未定义行为(返回一个新建对象的引用)”,“资源泄漏”,“结果出错”要好得多了。)
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子类也可以实现上述代码的操作。
释义:如果一个成员函数调用了其他的成员函数,那么就要用一个非成员函数替换这个成员函数。
根据条款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 中。于是,我们可以更为轻松地拓展这些便利函数——多做一些排列组合。
总结:若一个成员函数调用其他成员函数,那么这个成员函数的非成员函数版本比之拥有更多的封装性,和机能扩充性。
举例:有理数类和整数的运算
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 对象运算。
2onehalf 是 2 调用了operator。因为 2 是需要被转换的参数,而 2 的位置和 this(调用operator ) 对象的位置是一样的,所以无法将 2 转换成 Rational 类型,也就无法调用 operator 函数。
解决办法:使用 non-member 函数,让左右参数的地位平等:
const Rational operator*(const Rational& left_number, const Rational& right_number)
{...}
总结:如果所有参数(运算符左边或者右边的参数)都需要类型转换,用 non-member 函数。
周所周知,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,以便在更多的语境下使用
最好延后定义式,且在确定需要它在定义;
可以避免构造非必要对象,还可以避免无意义的默认构造行为;
string encryptPassword(const string& password) {
//string encrypted;
//encrypted = password;
// 修改,直接使用构造初始,避免无意义的默认构造
string encrypted(password);
encrypt(encrypted);
return encrypted;
}
建议:
尽可能延后变量定义式,可增加程序的清晰度并改善程序效率;
C++规则的设计目标之一式要保证类型错误绝不可能发生,即不在任何对象上执行不安全的操作;
C++不建议,但提供四种方式:
const_cast:
用来将对象的常量性转除;
dynamic_cast:
向下转型,决定某对象是否归属继承体系中的某个类型;速度慢,应尽量避免使用;
不能用于缺乏虚函数的类型;
reinterpret_cast:
执行低级转型,如pointer to int转成int;
转换结果都是执行期定义,代码移植性差;
static_cast:
强迫隐式转换,如non const转成const,int转为double;
建议:
尽量避免转型,特别在注重效率的代码中避免使用dynamic_cast;
若转型是必要的,则需要将它隐藏于某个函数后面;
class A{
public:
A();
~A();
string& getName() const { return m_name; }
private:
string m_name;
};
如上述所示,通过reference将私有的数据成员返回,有可能会降低数据的安全性,到时候外部通过reference修改数据;
改进:
class A{
public:
A();
~A();
const string& getName() const { return m_name; }
private:
string m_name;
};
建议:
避免返回handles指向对象内部;
不泄漏任何资源;
不允许数据败坏;
对象或数据结构不被破坏;
程序状态不改变;
绝不抛出异常;
建议:
异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏(基本型、强烈型、不抛异常型);
1.内联函数不易过多的原因:
2.内联函数动作像函数,比宏好得多,调用它不需消耗像函数一样的开销;
inline只是对编译器的申请,不是强制命令,编译器会根据函数的实际功能做出判断是否为内联函数;
该函数一般被置于头文件中;
但inline不能随程序库的升级而升级,若修改则都必须重新编译;
3.建议:
将大多数inline限制在小型、被频繁调用的函数。便于日后调试和二进制升级,提升程序速度;
不要只因为function templates出现在头文件,就将它们声明为inline;
1.编写class接口时,尽可能的将接口从实现中分离出来
class Person
{
public:
Person(const string& name, const Date& day, const Address& addr);
string getName() const;
string getday() const;
string getaddr() const;
private:
string m_name;
Date m_day;
Address m_addr;
};
当编译器编译的时候,需要获取string、Date、Address的定义式,才能知道该对象的大小,好让编译器分配多少内存;
上述代码中,该类与include的文件形成一种编译依赖的关系,一旦这些依赖头文件有所改变,则任何使用该Person类的文件也必须重新编译;
一旦在开发过程中,头文件的数量众多,将会浪费大量的时间;
使用指针来确定编译器间对象的大小
由于在编译期间需要确定对象的大小,故使用指针来指向该对象,即编译器不用对该对象一探究竟,知道该指针的大小;
使用智能指针指向实现物,从而将接口与实现分离,故在类种做修改不需要使用时重新编译;
class Person
{
public:
Person(const string& name, const Date& day, const Address& addr);
string getName() const;
string getday() const;
string getaddr() const;
private:
std::shared_ptr<PersonImpl> pImpl;
};
设计策略:
如果使用object references或object pointers可以完成任务,就不要使用object;
如果能够,尽量以class声明式替换class定义式;
当声明函数需要使用某个类时,只需将其声明即可;
为声明式和定义式提供不同的头文件,让接口与实现分离,实现修改不需要客户重新编译;
Handle class
让Person成员函数调用pImpl的函数,接触实现与接口之间的耦合关系;
该举措不会改变Person做到事,指挥改变其做事的方法;
**#include "Person.h"
#include "PersonImpl.h"
Person::Person(const string& name, const Date& day
, const Address& addr)
: pImpl(new PersonImpl(name, day, addr)) {}
string Person::getName() const {
return pImpl->getName();
}**
Interface class
通过将Person变成抽象基类,为子类提供接口,只有在Person类接口被修改才需要重新编译;
class Person
{
public:
virtual ~Person();
virtual string getName() const = 0;
virtual string getday() const = 0;
virtual string getaddr() const = 0;
};
class RealPerson : Person {
public:
RealPerson(const string& name, const Date& day, const Address& addr)
: m_name(name), m_day(day), m_addr(addr){}
virtual ~RealPerson();
string getName() const;
string getday() const;
string getaddr() const;
private:
string m_name;
Date m_day;
Address m_addr;
};
建议:
支持编译依存性最小化的一般构想:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle和interface classes;
程序库头文件应该以完全且仅有声明式的形式存在;
在C++中public inheritance意味着is-a的关系;
使用public继承,可以使用base class类型接收derived class对象;
好的接口可以防止无效代码通过编译,需要在编译期拒绝发生错误的设计;
建议:
public继承意味着is-a,适用于base-classes身上的一定适用于derived classes身上;
内层作用域的名称会遮掩外围作用域的名称,仅名称,与数据类型,函数参数无关;
编译器会查找作用域,由内到外;
如何避免函数被遮掩
class Base {
public:
virtual void func1() = 0;
virtual void func1(int);
void func2();
void func2(double);
virtual ~Base() {}
};
class sun : public Base {
public:
//1、当在函数中使用using即可解除遮掩
using Base::func1;
using Base::func2;
virtual void func1(); // sun::func1遮掩Base::func1(int)
void func2(); // sun::func2遮掩了Base::func2(double)
virtual ~sun() {}
};
建议:
derived classes内的名称会遮掩base classes内的名称;
为了避免被遮掩,可使用using声明或转交函数(前面添加virtual);
当使用public继承时:
成员函数的接口总是会被继承的;
纯虚函数虚被继承的重新声明,且在base class中没有定义,为了让子类只继承接口;
虚函数是为了让子类继承该函数的接口和缺省实现,必须继承,若不想写,则使用父类的;
non-virtual函数是为了令derived classes继承函数的接口及一份强制性实现;
三种函数之间的差别:
pure virtual函数:只继承接口;
virtual函数:继承接口和一份缺省实现;
non-virtual函数:继承接口和一份强制实现;
建议:
接口继承和实现继承不同;
non-virtual interface实现teplate method模式:
让用户通过public non-virtual成员函数直接调用private virtual函数;
该non-virtual函数为virtual的外覆器;
该外覆器确保在调用virtual之前将设定好事前工作【加锁,验证等】,在结束后做清理工作【解锁,解除约束等】;
class GameChar {
public:
int getVal() const {
// ....
int ret = _getVal();
// ...
return ret;
}
private:
virtual int _getVal() const {
// ....
}
};
**使用Function Pointer实现策略模式**
class GameChar;
int defaultHeadlthCalc(const GameChar& gc);
class GameChar {
public:
typedef int(*HCalcFunc)(const GameChar&);
explicit GameChar(HCalcFunc hcf = defaultHeadlthCalc)
: hFunc(hcf) {}
int getVal() const {
return hFunc(*this);
}
private:
HCalcFunc hFunc;
};
// 派生类
class EvilBadGuy : public GameChar {
public:
explicit EvilBadGuy(HCalcFunc hcf = defaultHeadlthCalc)
: GameChar(hcf){ }
};
// 即刻自定义,创建对象时通过构造函数传入
int myHfunc(const GameChar&);
EvilBadGuy eb(myHfunc);
std::function替代function Pointer
当使用std::function即可保存任何函数类型;
class GameChar;
int defaultHeadlthCalc(const GameChar& gc);
class GameChar {
public:
typedef std::function<int(const GameChar&)> HCalcFunc;
explicit GameChar(HCalcFunc hcf = defaultHeadlthCalc)
: hFunc(hcf) {}
int getVal() const {
return hFunc(*this);
}
private:
HCalcFunc hFunc;
};
virtual函数是动态绑定,而non-virtual函数是静态绑定;
当重定义non-virtual函数时,使用多态则对象调用的始终是base class的函数;
故在任何情况下都不该重新定义一个继承而来的non-vitrual函数;
建议:
绝不要重新定义继承而来的non-virtual函数;
建议:
-复合的意义和public继承完成不同;
在应用域,复合意味has-a。在实现域复合意味着is-implemented-in-terms-of;
当子类以private继承时编译器不会自动将一个子类转化为基类,基类的成员到子类中都将是private属性;
尽可能使用复合,必要时在使用private
当牵扯到protected成员和virtual函数时;
当空间方面的利害关系;
为了实现某个功能而继承,但避免不然接口误用
有一个定时器类,当一个类需要用到改类的virtual此时又要避免用户使用时,调用到Timer的接口,故将使用private继承;
class Timer {
public:
explicit Timer(int t);
virtual void onTick() const;
};
//改进,使用复合设计,且将该类在编译的依存性降至最低;
class Timer {
public:
explicit Timer(int t);
virtual void onTick() const;
};
class WidgetTimer : public Timer {
virtual void onTick() const;
};
class Widget {
WidgetTimer* timer;
};
空白基类最优化
使用private继承可以节约内存空间。当面临空基类情况时,用private可以实现空白基类最优化(EBO),节约了空间;
建议:
private继承意味is-implemented-in-terms of,通常比复合的级别低。适用于子类需要访问protected父类的成员。或需要重新定义继承而来的virtual函数;
和复合不同,private继承可造成empty base最优化;
最为显著的例子:菱形继承
virtual base的初始化责任时继承体系中最底层的class负责:
尽量少在base class内放置数据;
虚继承如何解决该继承问题:
解决菱形方案,操作的是共享的一份数据。
vbptr 虚基类指针;
指向一张虚基类表;
通过表找到偏移量;
找到共有的变量。
建议:
多重继承比单一继承复杂,它可能导致新的歧义性,以及对vrtual继承的需要;
virtual继承会增加体积大小、速度、初始化等成本,若virtual base classes不带任何数据,将最具实用价值;
多重继承的确有正当用途,可用于:
public继承某个Interface class和private继承某个协助实现的class;
编译期多态:函数调用造成template具现化,以不同的模板参数具现函数模板从而调用不同的函数;
建议:
classes和templates都支持接口和多态;
对classes而言接口是显式的,以函数签名为中心,多态则是通过virtual函数发生于运行期;
对于template参数,接口时隐式的,多态是通过template具现化和函数重载解析发生于编译期;
class和typename有什么不同:
1.两者的意义完全相同;
2.但有些情况需要使用typename:当你想要载template中指涉一个嵌套从属类型名称(迭代器),就必须在紧临它的前一个位置放上关键字;
template<typename C>
void test(const C& container, typename C::iterator iter) {// 嵌套从属类型名称
if (container.size() >= 2) {
typename C::const_iterator iter(container.begin()); // 嵌套从属类型名称
}
}
typename不可以出现在base classes list内的嵌套从属类型名称前,也不可以在member initlization list中作为base class修饰符;
template<typename T>
class sun : public Base<T>::Nested { // base classes list
public:
explicit sun(int x) : Base<T>Nested(x) { // member initlization list
}
};
可使用typedef为使用typename的嵌套从属类型名称起别名;
建议:
声明template参数时,前缀关键字class和typename可互换;
请使用关键字typename标识嵌套从属类型名称;但不可以出现在base classes list内的嵌套从属类型名称前,也不可以在member initlization list中作为base class修饰符;
class CompanyA {
public:
void sendClearTxt(const string& msg);
void sendEncryted(const string& msg);
};
class CompanyB {
public:
void sendClearTxt(const string& msg);
void sendEncryted(const string& msg);
};
template<typename Company>
class MsgSender {
public:
void sendClear(string msg) {
Company c;
c.sendClearTxt(msg);
}
};
// 编译器不明确自己要继承的基类,故LoggingMsgSend无法被具现化
// 由于基类模板可能被特化且和一般版本提供的接口不同
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
void sendClearMsg(const string& msg) {
sendClear(msg);
}
};
当继承基类时避免不进入templatized base classes观察行为失效时:
在base class函数调用动作之前加上this->;
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
void sendClearMsg(const string& msg) {
this->sendClear(msg);
}
};
使用using声明式;
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
using MsgSender<Company>::sendClear;
void sendClearMsg(const string& msg) {
sendClear(msg);
}
};
明白指出被调用的函数位于base class内;
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
void sendClearMsg(const string& msg) {
MsgSender<Company>::sendClear(msg);
}
};
建议:
可在derived class templates内通过this->指涉base class templates内的成员名称,或写出base class资格修饰符;
建议:
templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系;
因非类型模板参数而造成的代码膨胀,往往可消除,以函数参数或class成员变量替换template参数;
因类型参数而造成的代码膨胀,往往可降低,让带有完全相同二进制表述的具现类型共享实现码;
以下为一个构造模板
template<typename T>
class Test{
public:
template<class I>
Test(const Test<I>&); // 成员模板
};
成员函数模板并不会改变语言基本规则;
当提供一个泛化的构造函数并不会阻止编译器生成一个构造函数;
建议:
请使用member function template生成可接受所有兼容类型的函数;
如果你声明member template用于泛化copy构造或赋值操作,还是需要声明正常的copy构造函数和copy assignment操作符;
template<typename T>
class Rational {
public:
friend const Rational operator*(const Rational& lhs,
const Rational& rhs){
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
};
建议:
当我们编写一个class template,而它提供之与此template相关的函数支持所有参数之隐式类型转换时,请将那些函数定义为class template内部的friend函数;
如何设计traits class:
确认若干你希望将来可取得的类型相关信息;
为该信息选择一个名称;
提供一个template和一组特化版本,内含你希望支持的类型相关信息;
如何使用trait class:
建立一组重载函数或函数模板,彼此间的差异只在于各自的train参数。令每个函数实现码与其接受之traits信息相应和;
建立一个控制函数或函数模板,它调用上述那些函数并传递traits所提供的信息;
/** 类的萃取机 */
template<class I>
struct iter_traits {
typedef typename I::iter_category iterator_category;
typedef typename I::value_type value_type;
typedef typename I::differ_type differ_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
};
/** 指针萃取机 */
template<class T>
struct iter_traits<T*> {
typedef random_access_iterator_tag iter_category;
typedef T value_type;
typedef ptrdiff_t differ_type;
typedef T* pointer;
typedef T& reference;
};
/** const 萃取机 */
template<class T>
struct iter_traits<const T*> {
typedef random_access_iterator_tag iter_category;
typedef T value_type;
typedef ptrdiff_t differ_type;
typedef const T* pointer;
typedef const T& reference;
};
template<class T>
class A{
public:
A() {}
typedef T value_type;
typedef T iter_category;
typedef ptrdiff_t differ_type;
typedef T* pointer;
typedef T& reference;
private:
};
void use_class() {
int a = 10;
iter_traits<A<int> >::value_type vt = 10;
iter_traits<A<int> >::pointer p = &a;
iter_traits<A<int> >::differ_type dt = a;
iter_traits<A<int> >::reference r = a; // 引用必须初始化
iter_traits<A<int> >::iterator_category ic = a;
}
void use_pointer() {
int a = 10;
iter_traits<int*>::value_type vt = 10;
iter_traits<int*>::pointer p = &a;
iter_traits<int*>::differ_type dt = a;
iter_traits<int*>::reference r = a; // 引用必须初始化
iter_traits<int*>::iter_category ic;
}
void use_const_pointer() {
int a = 10;
iter_traits<const int*>::value_type vt = 10;
iter_traits<const int*>::pointer p = &a;
iter_traits<const int*>::differ_type dt = a;
iter_traits<const int*>::reference r = a; // 引用必须初始化
iter_traits<const int*>::iter_category ic;
}
建议:
Traits classes使得类型相关信息在编译期可用。它们以templates和templas特化完成实现;
整合重载技术后,traits classes有可能在编译期对类型执行if else测试;
元编程让某些编程更简洁;
工作由运行期转移到编译器;
达到好的效果:
确保度量单位正确;
优化矩阵运算;
可以生成客户定制之设计模式实现品;
建议:
可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率;
TMP可被用来生成基于政策选择组合的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码;
1.当operator new抛出异常以反映一个未满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler。那是声明于的个标准程序库函数
2.当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
typedef void(*new_handler)();
new_handler set_new_handler(new_handler p)throw();
new-handle做的事情:
让更多内存可被使用;
安装另一个new-handle:若无法获取更多内存,则下次调用就要做不同事;
卸除new-handle:将null指针传给set_new_handler;
抛出bad_alloc的异常;
不返回:调用abort或exit;
建议:
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用;
nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常;
1.operator new应该内含一个无穷循环,并在其中尝试分配内存,若无法满足,则调用new-handle;
2.operator delete应在受到null指针时,不做任何事;
严肃对待编译器发出的警告信息;
不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度不同;
tr1组件:
可处理多种问题:
参考: