C++基础进阶

引用与指针

C++中引入了引用操作,在对引用的使用加了更多限制条件的情况下,保证了引用使用的安全性和便捷性,还可以保持代码的优雅性。在适合的情况使用适合的操作,引用的使用可以一定程度避免“指针满天飞”的情况,对于提升程序稳定性也有一定的积极意义。最后,指针与引用底层实现都是一样的,不用担心两者的性能差距。

纯虚函数和抽象类

  • 纯虚函数:没有函数体的虚函数
  • 抽象类:包含纯虚函数的类
  • C++中的纯虚函数(或抽象函数)是我们没有实现的虚函数!我们只需声明它!通过声明中赋值0来声明纯虚函数!
  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象!

如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类。

  1. 纯虚函数使一个类变成抽象类
  2. 抽象类类型的指针和引用
  3. 如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类。
  4. 抽象类可以有构造函数
  5. 构造函数不能是虚函数,而析构函数可以是虚析构函数。
  • 完整例子
#include <iostream>
using namespace std;

class Base {
  int x;

public:
  virtual void fun() = 0;
  int getX() { return x; }
};

class Derived : public Base {
public:
  void fun() { cout << "fun() called"; } // 实现了fun()函数
};

int main(void) {
  Derived d;
  d.fun();
  return 0;
}

C++虚函数的vptr与vtable

为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。

虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数

其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针

因此,它使每个类对象的分配比原来大一个指针的大小。这也意味着vptr由派生类继承,这很重要。

#include <iostream>
#include <stdio.h>
using namespace std;

/**
 * @brief 函数指针
 */
typedef void (*Fun)();

/**
 * @brief 基类
 */
class Base {
public:
  Base(){};
  virtual void fun1() { cout << "Base::fun1()" << endl; }
  virtual void fun2() { cout << "Base::fun2()" << endl; }
  virtual void fun3() {}
  ~Base(){};
};

/**
 * @brief 派生类
 */
class Derived : public Base {
public:
  Derived(){};
  void fun1() { cout << "Derived::fun1()" << endl; }
  void fun2() { cout << "DerivedClass::fun2()" << endl; }
  ~Derived(){};
};

/**
 * @brief
 * 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表
 *
 * @param obj
 * @param offset
 *
 * @return
 */
Fun getAddr(void *obj, unsigned int offset) {
  cout << "=======================" << endl;
  void *vptr_addr =
      (void *)*(unsigned long *)obj; // 64位操作系统,占8字节,通过*(unsigned
                                     // long *)obj取出前8字节,即vptr指针
  printf("vptr_addr:%p\n", vptr_addr);

  /**
   * @brief 通过vptr指针访问virtual
   * table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned
   * long *)vptr_addr取出前8字节, 后面加上偏移量就是每个函数的地址!
   */
  void *func_addr = (void *)*((unsigned long *)vptr_addr + offset);
  printf("func_addr:%p\n", func_addr);
  return (Fun)func_addr;
}
int main(void) {
  Base ptr;
  Derived d;
  Base *pt = new Derived(); // 基类指针指向派生类实例
  Base &pp = ptr;           // 基类引用指向基类实例
  Base &p = d;              // 基类引用指向派生类实例
  cout << "基类对象直接调用" << endl;
  ptr.fun1();
  cout << "基类引用指向基类实例" << endl;
  pp.fun1();
  cout << "基类指针指向派生类实例并调用虚函数" << endl;
  pt->fun1();
  cout << "基类引用指向派生类实例并调用虚函数" << endl;
  p.fun1();

  // 手动查找vptr 和 vtable
  Fun f1 = getAddr(pt, 0);
  (*f1)();
  Fun f2 = getAddr(pt, 1);
  (*f2)();

  delete pt;
  return 0;
}

virtual

  • 虚函数与运行多态
#include <iostream>
using namespace std;

class Employee {
public:
  virtual void raiseSalary() { cout << 0 << endl; }

  virtual void promote() { /* common promote code */
  }
};

class Manager : public Employee {
  virtual void raiseSalary() { cout << 100 << endl; }

  virtual void promote() { /* Manager specific promote */
  }
};
class Engineer : public Employee {
  virtual void raiseSalary() { cout << 200 << endl; }

  virtual void promote() { /* Manager specific promote */
  }
};

// Similarly, there may be other types of employees
// We need a very simple function to increment salary of all employees
// Note that emp[] is an array of pointers and actual pointed objects can
// be any type of employees. This function should ideally be in a class
// like Organization, we have made it global to keep things simple
void globalRaiseSalary(Employee *emp[], int n) {
  for (int i = 0; i < n; i++)
    emp[i]->raiseSalary(); // Polymorphic Call: Calls raiseSalary()
                           // according to the actual object, not
                           // according to the type of pointer
}
int main() {
  Employee *emp[] = {new Manager(), new Engineer};
  globalRaiseSalary(emp, 2);
  return 0;
}
  • 虚函数中默认参数,默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型
/**
 * @file first_example.cpp
 * @brief 虚函数中默认参数
 * 规则:虚函数是动态绑定的,默认参数是静态绑定的。默认参数的使用需要看指针或者应用本身的类型,而不是对象的类型!
 * @author 光城
 * @version v1
 * @date 2019-07-24
 */

#include <iostream>
using namespace std;

class Base {
public:
  virtual void fun(int x = 10) { cout << "Base::fun(), x = " << x << endl; }
};

class Derived : public Base {
public:
  virtual void fun(int x = 20) { cout << "Derived::fun(), x = " << x << endl; }
};

int main() {
  Derived d1;
  Base *bp = &d1;
  bp->fun(); // 10
  return 0;
}

const

const对象默认是文件局部变量,非const变量默认为extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。比如extern const int ext=12;

指针与const:

  1. 如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量
  2. 对于指向常量的指针,不能通过指针来修改对象的值。也不能使用void * 指针保存const对象的地址,必须使用const void *类型的指针保存const对象的地址。允许把非const对象的地址赋值给const对象的指针,如果要修改指针所指向的对象值,必须通过其他方式修改,不能直接通过当前指针直接修改。

函数中用const:对于非内部数据类型的输入参数,应该将值传递的方式改为const 引用传递,目的是提高效率。例如将void func(A a) 改为void func(const A &a)。对于内部数据类型的输入参数,不要将值传递的方式改为const 引用传递。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void func(int x) 不应该改为void func(const int &x)。

类中用const:略

static

当变量声明为static时,空间将在程序的生命周期内分配。即使多次调用该函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递。

this

对于Python来说有self,类比到C++中就是this指针

(1)一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。

(2)this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行

inline

inline是一种“用于实现的关键字,而不是用于声明的关键字

内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。

  1. 如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收货会更少!
  2. 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

volatile

  1. volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
  2. volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
  3. const 可以是 volatile (如只读的状态寄存器),指针可以是 volatile

::

  • 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
  • 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
  • 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

struct

这部分也没什么意思,我觉得C++中还是类这个概念最好用

struct与class

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

区别:

  1. 最本质的一个区别就是默认的访问控制
  2. 默认的继承访问权限。struct 是 public 的,class 是 private 的。
  3. struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

assert

void assert(int expression); 便于提前检查出错误然后报错

位域

具体内容要用到的时候再学习

位域 (Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的一个好处是它可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。第二个好处是位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。而这种数据结构的缺点在于,位段实现依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的

位域在内存中的布局是与机器有关的:位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定。取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域

extern

C++调用C函数,在C++中常在头文件见到extern “C"修饰函数 讲的问题感觉没什么意思

union

联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:

  • 默认访问控制符为 public
  • 可以含有构造函数、析构函数
  • 不能含有引用类型的成员
  • 不能继承自其他类,不能作为基类
  • 不能含有虚函数
  • 匿名 union 在定义所在作用域可直接访问 union 成员
  • 匿名 union 不能包含 protected 成员或 private 成员
  • 全局匿名联合必须是静态(static)的

sizeof

enum

explicit

friend

using

decltype