首先着重推荐这篇bolg,大神级总结https://blog.csdn.net/thefutureisour/article/category/1230423
C++ 是由C、面向对象、模板和STL组成,我们是站在C这个巨人的肩膀上进行的,C++ 引入类和对象的概念,进行了封装和抽象,非常重要的一点。模板由编译器自动执行,减少了代码的
冗长,提供复用性,不用提供所有的类型函数的重载版本。STL提供了优秀的C++模板库,以便程序员进行使用,特别棒。
const定义常量从汇编的角度来看,是给出了对应的内存地址,而不是和define一样是个立即数,所以cosnt定义的常量在程序运行过程中只有一份拷贝,而define定义的常量在内存中有若干个拷贝
例:
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
带参的宏函数只是简单的进行替换,用起来四处危机,尤其是展开的时候,特别容易出错,bug不断
const int* p = &a;/* p为指向常量的指针,即p指向的对象是常量,不可以通过*p = 3 来修改a的值,但这时p = &b换个指向还是可以的 */
int* const p = &a; /* p为常量指针,即p的指向不可更改,不可以通过p = &b来修改p的指向,但这时*p = 3改变a的值还是可以的 */
const int* const p = &a; /* p为指向常量的常量指针,p的指向以及指向的对象都不可以更改,无论是*p = 3,还是p = &b都是错误的 */
const vector::iterator表示这个迭代器的指向不可以更改,即表示的是常量迭代器
这种方式是可取的:
char& operator[] (size_t position)
{
return const_cast<char&>(
static_cast<const TestBlock&>(*this)[postion]);
}
-(1) 为内置型对象进行手工初始化,因为C++不保证初始化它们;
-(2) 构造函数最好使用成员初始化列表(实际初始化顺序不与列表的排列顺序有关,只取决于类中的声明顺序),而不要在构造函数体内使用赋值操作;
-(3) 未避免“跨编译单元的初始化次序”问题,请用local static代替non-local static对象。 不要使变量有全局的作用域,用局部static
c++的内置类型在定义时,会给你随机分配一个值,初值都是垃圾值,那么最佳的处理的方法在使用对象之前都将之初始化
初始化成员变量的顺序与列表排列的顺序没有关系,只是取决于声明这些成员变量的顺序,下面的代码中就是先a,后b,然后text,因此一定要注意写顺序。
class A
{
private:
int a;
double b;
string text;
public:
A():a(0), b(0), text("hello world"){} //构造函数
};
槽的方法,如下:
class A
{
private:
int a;
double b;
string text;
public:
A(){
a = 0;b = 0;text = "hello world";
}
};
因为在进入构造函数的函数体时,这些成员变量已经被初始化了,a和b初始化成垃圾值,string因为是STL,调用默认的构造函数初始化为空字符串,在函数体内进行的操作实为“赋值”,也就是用新值覆盖旧值。这也正是说它的执行效率不高的原因,既进行了初始化,又在之后进行了赋值,
EmptyClass a(b); // 调用的是拷贝构造函数
EmptyClass a = b; // 调用的是拷贝构造函数
EmptyClass a;
a = b; // 调用的是赋值运算符
这是由于const和引用会在需要声明的时候进行初始化,而编译器提供的默认构造函数是无法做到这一点的,只能显示的调用构造函数进行。
class HomeForSale
{
private:
HomeForSale(const HomeForSale& house){...}
HomeForSale& operator= (const HomeForSale& house){...}
};
class HomeForSale
{
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator= (const HomeForSale&);
};
class Uncopyable {
public:
Uncopyable() { }
~Uncopyable() { }
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable {
... //这个时候该class就不要再声明copy构造函数或copy assignment操作符了!
}
这样,任何人,甚至是member函数跟friend函数尝试拷贝HomeForSal对象的时候。编译器就试着生成HomeForSale的拷贝构造函数跟赋值操作符。但是这个时候要先调用基类的拷贝构造函数和赋值操作符。这时候,编译器就会拒绝这样调用。因为基类的拷贝构造函数式private。
如果没有virtual,在多态中,会出现内存泄漏,主要出现的原因是简单地析构了基类的部分,然后派生类被架空,delete后只释放了基类的内存,而派生类的内存没有释放。
2)若一个类不作为基类,或者不具备多态性(比如Uncopyable),就不该声明virtual析构函数。
加上virtual确实是可以防止多态过程中析构造成的内存泄露,但是一旦出现virtual,编译器就会出现vptr,这是需要占用空间的,因此出现多态继承时一定要加上virtual,非基类或者不用与多态的基类就不要加
3)纯虚函数,不能定义对象,不通过编译,就可以使得不想成为对象的类成为抽象类。但是有时难以找到一个纯虚函数,为了自然,可以将析构函数变为纯虚函数,如下:
class Shape
{
private:
int color;
public:
virtual ~Shape() = 0;
};
~Widget()
{
try
{…}
catch
{
记录出错的位置
终止程序
}
}
class Base
{
public:
Base()
{
VirtualFunction();
}
virtual void VirtualFunction()
{
cout << "In the Base Class" << endl;
};
};
class Derived: public Base
{
public:
void VirtualFunction()
{
cout << "In the Derived Class" << endl;
}
};
int main()
{
Derived d;
}
现象:上述例子中, 定义派生类对象时,会先构造基类,调用基类的构造函数,在基类的构造函数中调用了虚函数,如果按照多态的思路,行为的执行者应该是派生类的VirtualFunction(),也就是输出的是In the Derived Class,然而实际跑一下这个程序,运行结果却是In the Base Class。
本质:派生类部分必须是在基类部分构造完毕之后才会去构造,也就是说构造函数运行完毕后,函数进行返回,派生类才开始起作用,那么在base()中,只有派生类的VirtualFnction()运行结束才行,而现在虚函数表中也只有基类的VirtualFnction(),因此运行的结果是In the Base Class。
例如:int x,y,z;x=y=z;等号是右结合的,因此有x=(y=z); 如果没有返回值的话,这个等式是不成立的,不能通过编译,而返回引用不是编译器强制的
1) 返回引用可以节省资源,不必要为返回值调用构造函数
2)形如(x=y)=常数值这样的连等,编译器也能接受,这样的写法要求赋值运算符返回的是可以修改的左值
3)STL和boost库中的标准代码都是一个求德行,还是兼容着写吧,别胡搞了
4)+=、-=等与赋值相关的运算,最好也返回自身的引用
- 1)确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
- 2)确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
以下两种方法最好了
SampleClass& operator= (const SampleClass& s)
{
if(this == &s) return *this; //可以删掉
a = s.a;
b = s.b;
float* tmp = p; // 先保存了旧的指针
p = new float(*s.p); // 再申请新的空间,如果申请失败,p仍然指向原有的地址空间
delete tmp; // 能走到这里,说明申请空间是成功的,这时可以删掉旧的内容了
return *this;
}
SampleClass& operator= (const SampleClass& s)
{
if(this == &s) return *this; //可以删掉
a = s.a;
b = s.b;
float* tmp = new float(*s.p); // 先使用临时指针申请空间并填充内容
delete p; // 若能走到这一步,说明申请空间成功,就可以释放掉本地指针所指向的空间
p = tmp; // 将本地指针指向临时指针
return *this;
}
还有一种最屌的实现方法
SampleClass& operator= (const SampleClass& s)
{
SampleClass tmp(s);
swap(*this, tmp);
return *this;
}
SampleClass& operator= (const SampleClass s)
{
swap(*this, s);
return *this;
}
*this == s是地址的问题
this == &s才是王道
class SampleClass
{
private:
int a;
double d;
public:
SampleClass(const SampleClass& s):a(s.a),d(s.d)
{}
};
class Derived: public SampleClass
{
private:
int derivedVar;
public:
Derived(const Derived& d):SampleClass(d), derivedVar(d.derivedVar){}
Derived& operator=(const Derived& d)
{
SampleClass::operator=(d);
derivedVar = d.derivedVar;
return *this;
}
}
尽量不去使用C/C++的原生指针,我们很容易忘记delete的,那么智能指针就是最屌的,直接利用对象资源进行管理,在析构中进行资源的释放 例:这样看不出原始指针,直接将new出来的内存交给了一个“管理者”,然后这个管理者可以是下列两个中的一个,程序员自己可以忘记delete,当管理的生命周期接收自动释放资源,交换系统-1)为了防止资源泄露,使用RAII思想(获得资源时进行初始化),造函数中获得资源,并在析构函数中释放资源
-2) 两个常用的RAII类shared_ptr和auto_ptr,前者一般是更佳的选择
特别强势的一个指针,独占资源,不允许其他管理者插手,否则就退出管理,自己不管了,脾气还大的不行。
特性:auto_ptr不支持STL容器,因为容器要求“可复制”,但auto_ptr只能有一个掌握管理权。另外,auto_ptr也不能用于数组,因为内部实现的时候,用的是delete,而不是delete[]。
auto_ptr<int> boss1(new int(10)); // 这个时候boss1对这个资源进行管理
auto_ptr<int> boss2(boss1); // 这个时候boss2要插手进来,boss1很生气,所以把管理权扔给了boss2,自己就不管事了。所以boss2持有这个资源,而boss1不再持有(持有的是null)
1 auto_ptr<int> boss1(new int(10));
2 auto_ptr<int> boss2(new int(10));
这种情况下,两者都是持有资源的,因为new执行了两次,虽然内存空间里的初始值是一样的,但地址并不同,所以不算相同的资源。
1.int *p = new int(10);
2.auto_ptr<int> boss1(p);
3.auto_ptr<int> boss2(p);
执行到第三句时,弹出assertion failed,程序崩溃,这是由于p指向同一块资源,当boss2访问时,知道p的资源已经被占用了,则将发生assertion failed。
原始指针和智能指针的混用特别烂,一定要避免
是一个分享与合作的管理者,内部实现是引用计数的,每多引用一次,计数值就会+1,每一次引用的生命周期结束,计数值-1,直到计数为0时,说明内存中没有管理者了,最后管理这个内存的管理者就会将之释放。
std::tr1::shared_ptr pInv(createInvestment());
int daysHeld(const Investment* pi);
int days = daysHeld(pInv); //无法通过编译!
daysHeld需要的是Investment*指针,但是传入的却是一个类型为tr1::shared_ptr 的对象,那么类型匹配都不成功,肯定是无法通过编译的。
为此,我们需要暴露出原始资源,有两种方式进行解决
提供一个get成员函数,用来执行显式转换。例如tr1::shared_ptr和auto_ptr都提供一个get成员函数来返回智能指针内部的原始指针(副本):
int days = daysHeld(pInv.get());
一种特殊的方法:没有返回值,没有形参,重载了返回值的类型
operator T*() const
{
return ptr;
}
同样,暴露资源还是会出现一些问题,例如资源管理类离开作用域,销毁了资源,而外部有指针指向资源管理类的内部资源,如果还在使用,将会导致未定义的行为
1 int *p = new int(); // 申请一个int资源
2 int *p = new int[3]; // 申请连续三块int资源
那么对应的也有两种释放资源的方式
1 delete p; // delete指针所指向的单一资源
2 delete [] p; // delete指针所指向的多个连续资源块
总结下,如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。但是事实上,如果一般用错了,编译器也不会进行任何提示,也没有运行时错误,因此这个得程序员自己好好上心。
以独立语句将new出来的对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
int GetPriority();
void processWidget(shared_ptr pw, int priority);
//如果有这样的调用
processWidget(new Widget(), GetPriority());
//这里会发生隐式转化,但是为了更满足程序员的需求,显示一点,有了explicit,不允许隐式转换,编译出错,因此有了
processWidget(shared_ptr pw(new Widget), GetPriority());
//但是这样会发生内存泄露,因为不同的编译器参数的调用顺序是不确定的,那么可能的调用顺序是:
(1) new Widget
(2) GetPriority() 并将返回值传给priority
(3) pw接管newWidget,而后传给函数形参
如果第二步中发生了异常,new返回的指针就会遗失,并未置入shared_ptr中,创建资源后并未能够转换为资源管理对象,那么两个时间点之间可能发生了异常导致资源泄露。
正确的做法,直接在第一步就成功进行资源的连接,即使GetPriority()发生异常,pw也不虚,我照样能干我自己的事,释放资源
shared_ptr pw(new Widget);
processWidget(pw, GetPriority());
多考虑用户的使用,引导用户,让用户动最少的脑筋却能最佳地使用接口,进行防御式编程
- 好的接口容易被正确使用,不容易被误用;
- 促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容;
阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;
多多使用shared_ptr来代替原始指针
构造函数和析构函数进行,如果想要有堆的分配方式,还需要重载new、new[]、delete和delete[]运算符
绝不要返回一个指向局部对象的指针或者引用,也不要返回指向分配在堆上的对象,也不要返回全局或者静态对象。返回值还是pass by value比较好
如果不存在继承关系,protected的作用与private等同,除了本类之外,其他类或者类外都不能访问,但如果存在继承关系时,protected标识的成员变量,在它的子类中仍可以直接访问,所以封装性就会受到冲击。这时如果更改父类的功能,相应子类涉及到的代码也要修改,这就麻烦了。而如果使用private标识,则根据规则,所有private标识的变量或者函数根本不被继承的,这样就可以保护父类的封装性了,仍可以在子类中通过父类的成员函数进行访问。
void example(const A& parm)
{
A a;
fun(); // 这个fun()函数不会使用a
a = parm;
…
}
那么在上面的代码中,如果fun()抛出异常,对象a的构造函数就白调用了,浪费空间也浪费时间,因此有了:
void example(const A& parm)
{
fun();
A a;
a = parm;
…
}
但是这里面a调用了A的拷贝构造函数,也调用了赋值运算,也浪费,那么最终版如下:
void example(const A& parm)
{
fun();
A a(parm);
…
}
循环中的常量定义,实在外边好呢,还是里面呢?
分析一下,如果采用放在循环外的方法,代价是1次构造+1次析构+N次赋值;如果采用放在循环内的方法,代价是N次构造+N次析构。所以究竟哪个更好,还是要看构造函数、析构函数VS赋值操作,哪一个比较废。
但如果两方的代价差不多,还是推荐放在循环内的做法,因为这种做法使得a的作用域局限于for循环内,符合作用域能小就小的编程原则。
不清楚原理导致各种无脑bug,还很难发现,要使用就用下面几种
- const_cast(expression):取出常量属性
1.只是针对指针或者引用,包括this指针,不能用于普通变量
2.既可以将const->非const,也可以将非const->const,这个操作并不改变被转换对象的常属性
int a= 3;
const int* ca = &a;
int *pb = const_cast<int*> (ca);
其中ca仍是指向常量的指针,对它的操作,编译器会报错
例:
1 void SpecialWindow::OnResize()
2 {
3 static_cast(*this).OnResize();
4 …
5 }
static_cast生成的是一个临时的基类的对象,这个对象并不是真正组成派生类的那个基类,而是它的一个副本(这个副本的成员变量值与派生类对象中基类的成分是一样的,但地址不同),调用OnResize()变更的结果是这个副本的成员变量变了,但派生类中包含的基类的成员变量却是没有任何变化的。好了,这就是bug了,之后测试中会抓狂。
那么总结就是转型生成一个copy,一份副本,有时就不是你想要的,修正其实很简单,如下
1 void SpecialWindow::OnResize()
2 {
3 Window::OnResize(); // OK了,就这么简单
4 …
5 }
dynamic_cast是当想把基类指针转成派生类指针时用,这种转换可以保证安全性,当把两个不相干的类之间用dynamic_cast时,转换的结果将是空指针,同时若基类指针指向的对象只是基类本身时,对基类指针进行dynamic_cast向下转型到派生类,会得到空指针,防止进一步的操作。
其实这个有时用起来也比较废,这个在转型的过程中要判断是否相等,那么就有这个转型会对类名称进行strcmp,以判断向下转型是否合理,如果继承深度比较大,那么每一次的dynamic_cast将会进行多次strcmp,这将严重影响程序的执行效率。这还不如利用多态的虚函数特性,或者工厂模式都可以实现基类向子类的转换,指向子类即可。
避免返回handles(包括reference、指针、迭代器)指向对象内部,遵守这个条款可增加封装性,并将发生danglinghandles的可能性降至最低。如果有必要必须要返回handles,在编写代码时就一定要注意对象和传出handle的生命周期。
强烈型:在基本承诺的基础上,保证成功就是完全成功,失败也能回到之前的状态,不存在介于成功或失败之间的状态。也就是果断,要么做要么不做
方法:copy and swap,也就是说再修改对象之前,先创建一个副本,然后对副本进行操作,如果操作发生异常,异常发生在副本上,不会影响本尊,如果没有异常在进行swap。
这种方法代价是时间和空间,必须为每一个即将被改动的对象创造副本
1 void someFunc()
2 {
3 f1();
4 f2();
5 }
上例中,f1可能没异常,f2抛出异常,数据回到f2之前,但是程序猿想要回到f1之前,这样只能将f1和f2相结合才行,都nice着,就可进行copy and swap。但是代价需要改变函数的结构,代价大,破坏函数的模块性,如果不想这么做那就得放弃这个方法,将异常安全保障回退到基本保障。
inline是内敛,建议编译器将函数的每一个调用都用inline所声明的函数本体进行替换,以空间换时间,但是这种方式,会造成代码膨胀,优点是节省了函数调用的成本,直接进行替换。
一般的函数调用需要将之前的参数以堆栈的形式保存起来,调用结束后又要从堆栈中恢复那些参数。
但注意inline只是对编译器的一个建议,编译器并不表示一定会采纳
比如当一个函数内部包含对自身的递归调用时,inline就会被编译器所忽略。对于虚函数的inline,编译器也会将之忽略掉,因为内联(代码展开)发生在编译期,而虚函数的行为是在运行期决定的,所以编译器忽略掉对虚函数的inline。对于函数指针,当一个函数指针指向一个inline函数的时候,通过函数指针的调用也有可能不会被编译器处理成内联。
class Person
{
private:
int age;
public:
int getAge() const //内敛
{
return age;
}
void setAge(const int o_age); //类外定义,不是内敛
};
void Person::setAge(const int o_age)
{
age = o_age;
}
不对构造函数和析构函数设置为inline
因为空的构造函数内被编译写上对所有成员函数的初始化,如果设为inlien,那不是在每个成员函数或者变量出都会展开,造成大量的赋值
慎用inline,一旦修改,就要重新编译,就大多数inline限制在小型、被频繁调度的函数身上。建议一开始不设置任何函数为inline,测试之后,确实需要inline,再来。
具体说明,还是别人家的总结的好
第二部分
第三部分
抽离出父类与子类之间所有的共性,放入父类中,然后类中特有的部分在子类中实现
适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
遮掩问题分为变量遮掩与函数遮掩,其实本质都是名字的查找方式,当编译器要查找一个名字时,它一旦找到一个相符的名字,就不会放下找了,因此遮掩问题是优先查找哪个名字的问题。
其实遮掩就是个作用域问题,在作用域内先查找那个名称或变量,有以下的例子:
//例1:普通变量遮掩
int i = 3;
int main()
{
int i = 4;
cout << i << endl; // 输出4,优先查找局部作用域
}
那么在继承中,也是一样的,都是在各自的作用域内进行查找
//例4:重载函数的遮掩
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
void virtual PureVirtualFunction() = 0;
};
class Derived: public Base
{
public:
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
int main()
{
Derived d;
d.VirtualFunction(3); // 这里将会出现编译时错误,因为派生类出现了遮掩问题,在派生类的作用域中就搜索不到相应的函数,所以会报错
return 0;
}
//例5:采用using声明,使查找范围扩大至父类指定的函数:
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
void virtual PureVirtualFunction() = 0;
};
class Derived: public Base
{
public:
using Base::VirtualFunction; // 第一级查找也要包括Base::VirtualFunction
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
int main()
{
Derived d;
d.VirtualFunction(3); // 这样没问题了,编译器会把父类作用域里面的函数名VirtualFunciton也纳入第一批查找范围,这样就能发现其实是父类的函数与main中的调用匹配得更好(因为有一个形参),这样会输出Base::VirtualFunction() With Parms
return 0;
}
class BaseClass
{
public:
void virtual PureVirtualFunction() = 0; // 纯虚函数
void virtual ImpureVirtualFunction(); // 虚函数
void CommonFunciton(); // 普通函数
};
纯虚函数有一个“等于0”的声明,具体实现一般放在派生中(但基类也可以有具体实现),所在的类(称之为虚基类)是不能定义对象的,派生类中仍然也可以不实现这个纯虚函数,交由派生类的派生类实现,总之直到有一个派生类将之实现,才可以由这个派生类定义出它的对象。
普通函数既继承接口也强制继承实现
普通函数则是将接口与实现都继承下来了,如果在派生类中重定义普通函数,将会出现名称的遮盖。 我们极不推荐在派生类中覆盖基类的普通函数的,如果真的要这样做,请一定要考虑是否该把基类的这个函数声明为虚函数或者纯虚函数。
多用纯虚函数来代替虚函数,因为虚函数将会提供一份默认的实现,如果派生类想要的行为与基类不一样,但是又忘了进行覆盖虚函数,那就会问题。而纯虚函数就会杜绝这种情况,派生必须去实现它,否则无法定义。 同时纯虚函数也是有默认实现的,可以代替虚函数。
virtual函数的本质是由虚指针和虚表来控制,虚指针指向虚表中的某个函数入口地址,就实现了多态。因此,我们也可以仿照一个虚指针指向函数的手法,来做一个函数指针
如果派生类重新定义了一个非虚函数,即是每个类都有自己的想法,那用继承干嘛,自己子类都可以分离开来。
缺省参数值是静态绑定,virtual是动态绑定
enum MyColor
{
RED,
GREEN,
BLUE,
};
class Shape
{
public:
void virtual Draw(MyColor color = RED) const = 0;
};
class Rectangle: public Shape
{
public:
void Draw(MyColor color = GREEN) const
{
cout << "default color = " << color << endl;
}
};
class Triangle : public Shape
{
public:
void Draw(MyColor color = BLUE) const
{
cout << "default color = " << color << endl;
}
};
int main()
{
Shape *sr = new Rectangle();
Shape *st = new Triangle();
cout << "sr->Draw() = "; // ?
sr->Draw();
cout << "st->Draw() = "; // ?
st->Draw();
delete sr;
delete st;
}
在这个例子中有虚函数,如果父类中有虚函数,父类中将会生成一个虚指针和一个虚表,程序运行过程中,将会根据 虚指针的指向,来决定调用哪个虚函数,这称之为动态绑定,而与之相对应的就是静态绑定,是在编译器进行的。实现动态绑定的代价是非常大的,因此函数参数这部分都是静态绑定的,编译时就决定下来了。那么如果对继承而来的虚函数使用不同的缺省值,造成代码阅读者的困惑,因为缺省的参数在继承下来是不改变的。
复合就是在一个类中采用其他类的对象作为自身的成员变量,是一种拥有的关系,has-a。
在程序中,往往是需要依据某一个模块来实现另一个模块,一般来说想到了继承的方法,但是继承一般有一种法则就是子类将会替代父类的特性,不能更改,但是我们这个时候既想利用一个模块现有的特性,也不想违反可替代原则,那么就可以利用复合的关系。==最明显的就是STL中stack和queue都是利用deque来实现的,这就是一种依赖,一种复合==
#include
#include
using namespace std;
template <class T>
class MySet
{
private:
list MyList;
public:
int Size() const
{
return MyList.size();
}
bool IsContained(T Element) const
{
return (find(MyList.begin(), MyList.end(), T) != MyList.end());
}
bool Insert(T Element)
{
if (!IsContained(T))
{
MyList.push_back(Element);
return true;
}
else
{
return false;
}
}
bool Remove(T Element)
{
list ::iterator Iter = find(MyList.begin(), MyList.end(), T);
if (Iter != MyList.end())
{
MyList.erase(Iter);
return true;
}
else
{
return false;
}
}
};
public和private的不同:
class TypeDefine
{};
class SimpleClass
{
int a;
TypeDefine obj;
};
class SimpleDerivedClass : private TypeDefine
{
int a;
};
int main()
{
cout << sizeof(TypeDefine) << endl; // ?
cout << sizeof(SimpleClass) << endl; // ?
cout << sizeof(SimpleDerivedClass) << endl; // ?
}
第一个是空类,对于空类,编译器生成了四个默认的函数:默认构造函数、默认拷贝构造、默认析构函数和默认赋值运算符。但是这四个都不占空间,而类的每一个对象都需要一个独一无二的内存地址,所以编译器会在空类中插入一个1字节变量,正是这个1字节变量,才能区分空类的不同对象。对于非空类,编译器可以利用成员变量来进行内存地址的区分。
第二个输出是8,因为会有内存地址对齐的规则
第三个结果是4,因为有private继承,原本是要为空白类对象插入1字节的,但是因为子类中有了对象,这样理论上就可以凭借这个对象来进行区分识别了,这时编译器就不再插入字节了。
尽量选择单继承进行以免在项目中带来可读性的问题,==多重继承中容易碰到的问题就是名字冲突==,如下面的例子所示。 但是如果想要去访问重名的函数,那么可以指定作用域d.Base1::fun()进行,因此可以知道编译器会优先去查找最合适的重载函数,再去考虑它的可访问性。
class Base1
{
public:
void fun(){}
};
class Base2
{
private:
void fun(){}
};
class Derived : public Base1, public Base2
{};
int main()
{
Derived d;
d.fun(); // error C2385: 对“fun”的访问不明确
return 0;
}
为了解决多重继承中出现多个祖类成员的问题,那么有了虚拟继承,可以避免这个问题。假如说是:有一个父类名为A,类B和类C都继承于A,类D又同时继承了B和C(多重继承),那么如果不做任何处理,C++的类继承图里会包含两份A。采用了virtual继承后,D中就只有一份A了,如下所示:
class B: virtual public A{…}
class C: virtual pulibc A{…}
但是,为了保证不会出现两份父类,只要是public继承理论上都应该有virutal关键字,但virutal也是有代价的,访问virtual base class的成员变量要比访问non-virutal base class的成员变量速度要慢
因此有:
- 非必要不使用virtual classes继承,普通情况请使用non-virtual classes继承
- 如果必须使用virtual base classes,尽可能避免在其中放置数据。