封装,继承,多态
。
封装
封装最好理解。封装是面向对象的特征之一,是对象和类概念的主要特性。
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
继承
面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态
所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
主要有三种: 复合,委托,继承
has-a的关系
):构造由内而外,析构由外而内。
class Shape
{
virtual drow() const = 0; //纯虚函数
virtual printError(); //虚函数
int fun(); //普通函数
};
注意看:myDoc.Onfileopen->CDocument::onffileopen()
seriailze() -> this->seriailze()
而在他的类中是有定义该函数的
在这个表中,主要是一个类的虚函数的地址表(所以就是只要类中有virtual函数,就会有一张虚函数表
),这张表解决了继承、覆盖的问题,保证其能真实反应实际的函数。(一个类的虚函数表是静态 static 的,也就是说对这个类的每个实例,他的虚函数表的是固定的,不会为每个实例生成一个相应的虚函数表。只会在每个示例中添加一个指向虚函数表的指针
),所以,当我们用父类的指针来操作一个子类实例的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址可以得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数
。
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
所以用图来表示就是这样子:
注意:在上面这个图中,虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了
虚函数表的结束
。这个结束标志的值在不同的编译器下是不同的。
在WinXP+VS2003下,这个值是NULL。
而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
接下来,通过对于各种继承的情况来学习一下虚函数实现的本质。
在这个继承关系中,子类没有重写任何父类的函数。那么,在子类实例的虚函数表
如下所示:
虚函数按照其声明顺序放于表中。
父类的虚函数在子类的虚函数前面。( 父 —>> 子)
在这个类的设计中,只覆盖了父类的一个函数:f()。那么,对于派生类的实例的虚函数表会是下面的样子:
覆盖的f()函数被放到子类虚函数表中原来父类虚函数的位置。
没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表(子类的虚函数表)的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
确定 public 继承一定能够塑造出 is-a 的关系
。适用于base class身上的每一件事情一定也适用于derived class 身上。因为每一个derived class 就是一个 base class 对象。解释:绝不重新定义继承而来的缺省参数值和非虚(non-virtual)函数
主要原因:静态绑定与动态绑定
先来看一个发生的案例:
class A
{
public:
virtual void func(int val = 0)
{
cout << "A-> val== " << val << endl;
}
virtual void test()
{
func();
}
};
//A的虚表是:A::func A::test
class B : public A
{
public:
virtual void func(int val = 666)
{
cout << "B-> val== " << val << endl;
}
};
//A的虚表是:B::func A::test
int main()
{
B *p = new B;
p->test();
}
(1) 由于B类中没有覆盖(重写)基类中的虚函数test(),因此会调用基类A中的test();
(2) A中test()函数中继续调用虚函数 fun(),因为虚函数执行动态绑定,p此时的动态类型(即目前所指对象的类型)为B*,因此此时调用虚函数fun()时,执行的是B类中的fun();所以先输出“B->”;
(3) 缺省参数值是静态绑定,即此时val的值使用的是基类A中的缺省参数值,其值在编译阶段已经绑定,值为0,所以输出“0”;
最终输出“B->0”。所以大家还是记住上述结论:绝不重新定义继承而来的缺省参数值!
绝不定义重新定义继承而来的非虚(non-virtual)函数也是这个原因。因为non-virtual是静态绑定的,在编译器编译阶段就已经定型了,而virtual 是动态绑定的。在具体执行调用时,才进行把函数调用与响应调用所需要的代码相结合的动作。
更多设计与规划见:Effective C++
初始化列表
,explicit(显式构造),范围 for 循环,=default,=delete,nonexcept,decltype,override,final ,auto关键字,nullptr,模板别名(alias template),Static assertion(编译时期的断言) [capture list] (parameter list) ->return type { function body }
外部变量
[=] 传值
[&] 传引用
匿名函数,相当于一个匿名函数对象。
class Test
{
public:
Test(int tt) : i(tt), j(0) {}
int i;
int j;
};
int main()
{
sort(vv.begin(), vv.end(), [](const Test &lhs, const Test &rhs) {
return lhs.i < rhs.i;
});
for (auto i : vv)
cout << " i== " << i.i << " j == " << i.j << endl;
return 0;
}
五:关于智能指针
//要有一个拆解包输出的终点函数调用
void print()
{
cout << "OKOKOKOK " << endl;
}
template <typename T, typename... Types>
void print(const T &fristArg, const Types &... args) //就是规定的这种写法
{
cout << "1+n-1" << endl;
cout << fristArg << endl;
print(args...);
}
template <typename... Types>
void print(const Types &... args)
{
cout << "nnnnnnn" << endl;
//cout << fristArg << endl;
print(args...);
}
int main()
{
print(7.5, bitset<16>(377), 42);
}
一个占据内存中某个可识别的位置(也就是一个地址)的对象
。(可变的变量,能够 取地址& ,非临时对象)右值是一个 不 表示内存中某个可识别位置的对象的表达式。(临时对象)
举例:
在 C++11 之前右值是不能够被修改的,但是之后在一些特殊的情况下,右值被变得可修改,既然可以被修改,那么就会有引用存在,就存在右值的引用。
右值引用是用来支持转移语义的。转移语义(steal)可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
std::move 就是将一个左值转为右值。需要注意的就是转右值后,原来的对象是一定不能够被是使用了!!!
template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param); //四种类型转换要清楚
}
#include
#include
#include
#include
using namespace std;
void Process(int &i)
{
cout << "Process(int &i) " << i << endl;
}
void Process(int &&i)
{
cout << "Process(int &&i) " << i << endl;
}
void Forward(int &&i)
{
cout << "Forward(int &&i) " << i << endl;
Process(i);
}
int main(void)
{
int a = 0;
Process(1); //右值
Process(std::move(a)); //右值
Forward(2);//右值(这个会完美的转发到Process(右值)吗??????)
Forward(std::move(a));
//Forward(a);
return 0;
}
根据运行结果,可见这个玩意儿真的不能够将右值完美转发。So,std::forward就有用了。
void Forward(int &&i)
{
cout << "Forward(int &&i) " << i << endl;
Process(std::forward<int&&>(i));
}
#include
#include
#include
#include
using namespace std;
std::mutex mtx;
std::condition_variable cond;
bool tag = true; //需要等待的条件
//打印偶数
void printOdd()
{
for (int i = 2; i < 10; i += 2)
{
unique_lock<std::mutex> ulock(mtx);
//条件变量是一种全局的变量,需要用锁来保护
cond.wait(ulock, []() { return !tag; });
// 等待后,当前线程 占有锁
//如果当前线程被阻塞,cond.wait 操作会自动解锁使得其他线程得以运行
cout << "我是偶函数" << endl;
cout << i << endl;
tag = true;
// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
ulock.unlock();
cond.notify_one();
}
}
//打印奇数
void printEven()
{
for (int i = 1; i < 10; i += 2)
{
unique_lock<std::mutex> ulock(mtx);
cond.wait(ulock, []() { return tag; });
cout << "我是奇函数" << endl;
cout << i << endl;
tag = false;
ulock.unlock();
cond.notify_one();
}
}
int main(void)
{
std::thread th1(printEven);
std::thread th2(printOdd);
th1.join();
th2.join();
return 0;
}
使用C++11实现线程池的两种方法
C++ 常考面试点总结
解释:
const 在(*变量)的左边,即:const int *p .就不能通过(*p)来改变它的值。
const 在(*变量)的右边,即: int *const p .就不能改变指针的指向。