C++面向对象编程思想与C++11新特性

文章目录

  • 类与类之间都有什么关系?
      • 复合(`has-a的关系`):
      • 委托:
      • 继承(is-a的关系):
          • 继承+虚函数(从函数的调用权理解)
          • 虚函数的分类:
      • 虚函数的实现机制:
          • 虚函数表
          • 一般继承(无虚函数覆盖)
          • 一般继承(有虚函数覆盖)
          • 多重继承(无虚函数覆盖)
          • 多重继承(有虚函数覆盖)
          • 关于继承 Effective C++ 中需要遵守的规则:
  • C++11新特性
      • 语言方面
          • 1.`初始化列表`,explicit(显式构造),范围 for 循环,=default,=delete,nonexcept,decltype,override,final ,auto关键字,nullptr,模板别名(alias template),Static assertion(编译时期的断言)
          • 2.lambda 表达式
          • 3.智能指针
          • 4.可变参数模板
          • 5.左值,右值,右值引用,std::move ,深浅拷贝,std::forward(完美转发)
      • 标准库方面
          • 1.线程库std::thread
          • 2.std::function,std::bind 等等
          • 3.正则表达式库,字符串类字新增与其他类型互换的方法,如to_string(),stoi(),stol等,基于hash表的无序关联容器(unordered_map以及unordered_set),array,tuple,forward_list等等。
      • 其他面试考点总结
          • 1.static与const 的区别
          • static关键字至少有下列n个作用:
          • const关键字至少有下列n个作用:
          • 限定指针规则: **记法:左定值,右定向。**
          • 2.override,overload,overwrite 的区别

众所周知,面向对象程序设计思想有三大基本特性: 封装,继承,多态

C++面向对象编程思想与C++11新特性_第1张图片

  • 封装
    封装最好理解。封装是面向对象的特征之一,是对象和类概念的主要特性。
    封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  • 继承
    面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

  • 多态
    所谓多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。

类与类之间都有什么关系?

主要有三种: 复合,委托,继承

复合(has-a的关系):

构造由内而外,析构由外而内。

委托:

继承(is-a的关系):

C++面向对象编程思想与C++11新特性_第2张图片

继承+虚函数(从函数的调用权理解)
虚函数的分类:
  • non-virtual:不希望子类重新定义该函数
  • virtual:希望子类重新定义该函数,已有默认定义
  • pure virtual:希望子类一定要重新定义该函数,没有默认定义(java中的抽象类,无法实例化)
class Shape
{
    virtual drow() const = 0; //纯虚函数
    virtual printError();     //虚函数
    int fun();                //普通函数
};

C++面向对象编程思想与C++11新特性_第3张图片
注意看: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; }
};

所以用图来表示就是这样子:
C++面向对象编程思想与C++11新特性_第4张图片注意:在上面这个图中,虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。

  • 在WinXP+VS2003下,这个值是NULL。

  • 而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

接下来,通过对于各种继承的情况来学习一下虚函数实现的本质。

一般继承(无虚函数覆盖)

C++面向对象编程思想与C++11新特性_第5张图片
在这个继承关系中,子类没有重写任何父类的函数。那么,在子类实例的虚函数表如下所示:

C++面向对象编程思想与C++11新特性_第6张图片
我们可以看到下面几点:

  1. 虚函数按照其声明顺序放于表中。

  2. 父类的虚函数在子类的虚函数前面。( 父 —>> 子)

一般继承(有虚函数覆盖)

C++面向对象编程思想与C++11新特性_第7张图片
在这个类的设计中,只覆盖了父类的一个函数:f()。那么,对于派生类的实例的虚函数表会是下面的样子:

C++面向对象编程思想与C++11新特性_第8张图片

  1. 覆盖的f()函数被放到子类虚函数表中原来父类虚函数的位置。

  2. 没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();

b->f();

由b所指的内存中的虚函数表(子类的虚函数表)的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

多重继承(无虚函数覆盖)

C++面向对象编程思想与C++11新特性_第9张图片
对于子类实例中的虚函数表,是下面这个样子:
C++面向对象编程思想与C++11新特性_第10张图片
我们可以看到:

  1. 每个父类都有自己的虚表
  2. 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数覆盖)

C++面向对象编程思想与C++11新特性_第11张图片
C++面向对象编程思想与C++11新特性_第12张图片
我们可以看见,三个父类虚函数表中的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()
关于继承 Effective C++ 中需要遵守的规则:
  • 确定 public 继承一定能够塑造出 is-a 的关系。适用于base class身上的每一件事情一定也适用于derived class 身上。因为每一个derived class 就是一个 base class 对象
  • 绝不重新定义继承而来的非虚(non-virtual)函数。理所当然。
  • 绝不重新定义继承而来的缺省参数值。
  • 明智而谨慎的使用private,多重继承。

解释:绝不重新定义继承而来的缺省参数值和非虚(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();
}

C++面向对象编程思想与C++11新特性_第13张图片
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++

C++11新特性

语言方面

1.初始化列表,explicit(显式构造),范围 for 循环,=default,=delete,nonexcept,decltype,override,final ,auto关键字,nullptr,模板别名(alias template),Static assertion(编译时期的断言)
2.lambda 表达式
	[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;
}
3.智能指针

五:关于智能指针

4.可变参数模板
//要有一个拆解包输出的终点函数调用
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++面向对象编程思想与C++11新特性_第14张图片

5.左值,右值,右值引用,std::move ,深浅拷贝,std::forward(完美转发)
  • 左值 (lvalue, locator value) 表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。(可变的变量,能够 取地址& ,非临时对象)
  • 右值 (rvalue) 则使用排除法来定义。一个表达式不是 左值 就是 右值 。 那么,右值是一个 不 表示内存中某个可识别位置的对象的表达式。(临时对象)

举例:
C++面向对象编程思想与C++11新特性_第15张图片
在 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); //四种类型转换要清楚
}
  • std::forward ()
#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;
}

C++面向对象编程思想与C++11新特性_第16张图片
根据运行结果,可见这个玩意儿真的不能够将右值完美转发。So,std::forward就有用了。

void Forward(int &&i)
{
    cout << "Forward(int &&i) " << i << endl;
    Process(std::forward<int&&>(i));
}

标准库方面

1.线程库std::thread
  • 两线程交打印奇偶数。使用 std::thread
#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++面向对象编程思想与C++11新特性_第17张图片

  • 线程池的实现见:

使用C++11实现线程池的两种方法

2.std::function,std::bind 等等
3.正则表达式库,字符串类字新增与其他类型互换的方法,如to_string(),stoi(),stol等,基于hash表的无序关联容器(unordered_map以及unordered_set),array,tuple,forward_list等等。

其他面试考点总结

C++ 常考面试点总结

1.static与const 的区别
static关键字至少有下列n个作用:
  • static 变量(全局变量)存在于静态存储区,随进程的消亡而消亡
  • 在类中的static成员变量,函数属于整个类所拥有,对类的所有对象只有一份拷贝,但函数不接收this指针,因而只能访问类的static成员变量。
  • 限定访问权限。(限定在当前文件下,防止命名污染)
const关键字至少有下列n个作用:
  • 用来定义常量。
  • 保护。欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
  • 限定指针规则。
  • 从汇编角度理解,只有一份拷贝。节省内存(#define 只是简单替换,多分拷贝)
限定指针规则: 记法:左定值,右定向。
解释:
const 在(*变量)的左边,即:const  int  *p .就不能通过(*p)来改变它的值。
const 在(*变量)的右边,即: int *const p .就不能改变指针的指向。
2.override,overload,overwrite 的区别

你可能感兴趣的:(C,/,C++)