首页 > 技术文章 > C++笔记(面向对象高级编程--侯捷)

jakelin 2021-01-25 14:13 原文

视频链接:https://www.bilibili.com/video/BV14s411E772

别人的git:https://github.com/harvestlamb/Cpp_houjie

inline内联函数

在C/C++中,有些函数会被频繁的调用,会有频繁的函数入栈、出栈操作,这样就造成一定程度上时间以及空间的消耗。于是在C++中便提出了内联函数的概念。

内联函数的特征:

  • 在编译时,编译器使用函数的定义替换掉函数的调用语句,这种替代行为发生在编译阶段而非程序运行阶段
  • 在类声明中定义的函数, 除了虚函数的其他函数都会自动隐式的成为内联函数
  • 关键字inline必须与函数定义体放在一起才能使函数成为内联, 仅将inline放在函数声明前不起任何作用

优点:

  • 省去函数调用时的参数压栈, 栈帧开辟, 结构返回等步骤, 从而提高了程序的运行速度
  • 内联函数优于宏,宏不能按值传递,并且内联函数可以访问类的成员变量
  • 内联函数在运行时可以调试, 而宏定义则不行(因为宏定义是被预定义处理的, 所以不会有人黑的编译符号和调试信息, 调试的时候基本只能用肉眼去看)

缺点:

  • 代码膨胀,内联函数增大了可执行程序的体积
  • C++内联函数的展开是中编译阶段,这就意味着如果你的内联函数发生了改动,那么就需要重新编译代码
  • 是否内联是程序员不可控的,inline 只是对编译器的建议,最终有编译器决定

构造函数(初始化、赋值)

构造函数可以分为两步:

  • 初始化
  • 赋值

对比以下两种方式的构造函数,方式一在直接通过参数初始化变量;而方式二先初始化,然后再对变量进行赋值。显然效率就有差异。

// 方式一:初始化
Complex(double r = 0, double i = 0) : re(r), im(i) { }

// 方式二:赋值
Complex(double r = 0, double i = 0) {
	re = r;
	im = i;
}

const

常量

关键字 const 限定常量只读。仅在声明时进行初始化,若声明变量时没有提供值,则该常量的值将是不确定的,且无法修改。

const 优于 #define

  1. const 能够明确指定类型
  2. 可以使用C++的作用域规则将定义限定在特定的函数或文件中
  3. 可以将 const 用于更复杂的类型

const函数

通常我的理解是用 const 修饰函数也就是标识函数不会修改数据。此时有了新的理解。先看简略代码如下:

// 类的部分定义
class Complex{
public:
	Complex(double r = 0, double i = 0) : re(r), im(i) { }
	double real() /* const */ { return re; }
	double imag() /* const */ { return im; }	
private:
	double re, im;
};
// 类对象的使用
int main(){
	const Complex c(1,2); // 常量对象
	cout << c.real() << " " << c.imag(); // 函数调用可能修改值,矛盾!编译器
	return 0;
}

我们以上面代码来分析为什么要用 const ,我们定义的常量复数类对象打印输出复数实部和虚部,假如两个函数没有 const 修饰,意思是函数可能改数据,但是对象是常量,不允许改数据,这是 矛盾的 ,那么编译器表示不行。

const object(data members 不可改变) non-const object(data members 可改变)
const member functions
non-const member functions ×

当成员函数的 constnon-const 版本同时存在,const object 只会(只能)调用 const 版本,non-const object 只会(只能)调用 non-const 版本。

friend友元

友元函数可直接访问私有成员

同一个 lass 的各个 objects 互为友元。 (如:拷贝构造函数)

友元关系不可传递不可继承。(你是我的朋友,但你的朋友不一定是我的朋友)

拷贝构造、拷贝赋值

class 带有 pointer members 必须有拷贝构造(深拷贝)拷贝赋值

inline
String::String(const char* cstr) { // 拷贝构造
   if (cstr) {
      m_data = new char[strlen(cstr)+1];
      strcpy(m_data, cstr);
   }
   else {   
      m_data = new char[1];
      *m_data = '\0';
   }
}

inline
String& String::operator=(const String& str) { // 拷贝赋值
   if (this == &str)
      return *this;

   delete[] m_data;
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
   return *this;
}

构造函数、析构函数

new :先分配内存 memory ,再调用构造 constructor

String* ps = new String("Hello");

编译器转化为:

String* ps;

void* mem = operator new(sizeof(String)); // 分配内存 内部调用 malloc
ps = static_cast<String*>(mem);			// 转型
ps->String::String("Hello");			//构造函数

delete :先再析构 destructor ,再释放内存 memory

delete ps;

编译器转化为:

String::~String(ps); 			// 析构函数
operator delete(ps);			// 释放内存 内部调用 free(ps)

动态分配所得内存块(in VC)

创建Complex对象、String对象,编译器(VC)给两对象分配内存如下:

动态分配所得内存块.png
  • 绿色部分对象实际占有的空间

  • 红色部分cookie,编译器在对象内存的头尾插入cookie,cookie值表示内存块大小以及状态,如图中:

    cookie = 0x00000041

    内存块大小 64 字节,对应十六进制数 0x40

    内存块是已分配的(给出去了)cookie最后一位为 1 标识,由此的到cookie值 0x00000041

  • 灰色部分是debug模式下编译器分配空间时需要插入的字节,VC下为 4*8+4 字节

  • 青色部分字节对齐(VC为16字节对齐)

对于数组array,内存块如下:

动态分配array所得内存块.png

array new 一定要搭配 array delete,下图给出内存泄露位置:

array-new一定要搭配array-delete.png

复合、委托、继承

Composition (复合):

class queue {
	deque c; // 复合 (包含对象)
}
  • 表示 has-a 关系。
  • 构造函数由内向外
  • 析构函数由外向内

Delegation (委托):

个人理解有点文件存储node结点的味道。引用计数。写时复制,读时共享。

class String {
	StringRep* rep; // 指针 指向实现功能的类
}

Inheritance (继承):

  • 表示 is-a 关系(是一种)
  • 构造函数由内而外(父类---子类)
  • 析构函数由外而内(子类---父类)、父类析构必须为 virtual

复合(Composition)+继承(Inheritance)

析构函数顺序与构造函数恰好相反。

  • 情况一

    构造函数 :Base ---> Composition ---> DerivedInheritance_Composition关系下的构造和析构_一_.png

  • 情况二(直接推导即知)

    构造函数 :Composition ---> Base ---> Derived

    Inheritance_Composition关系下的构造和析构_二_.png

虚函数

non-virtual 函数:非虚函数,不希望派生类重新定义(override)它;

vitual 函数:虚函数,希望派生类重新定义,且已有默认定义;

pure virtual 函数:纯虚函数,希望派生类一定要重新定义它,且,没有默认的定义。

智能指针

仿函数

仿函数(functors)另名函数对象(function objects)。

函数有函数名称,用一个 () 作用上去,任何一个东西如果可以接受 () 操作符,我们就称它 function-like 。每个仿函数都是某个类重载 () 运算符,然后变成了“仿函数”,实质还是一个类,但看起来具有函数的属性。

() 的重载,都继承了 unary_functionbinary_function

template <class _Arg, class _Result>
struct unary_function {  // 一元函数
  typedef _Arg argument_type;
  typedef _Result result_type;
};

template <class _Arg1, class _Arg2, class _Result>
struct binary_function {  // 二元函数
  typedef _Arg1 first_argument_type;
  typedef _Arg2 second_argument_type;
  typedef _Result result_type;
};  

模板泛化、特化

泛化

template <class Key>
struct hash { };

特化:模板特化指的是模板中指定特定的数据类型,这和泛化是不同的

template<>
struct hash<int> {
	size_t operator()(int x) const { retrun x; }
};

模板特化也有程度之分,可以部分类型指定,称之为偏特化

C++11

数量不定的模板参数

... 就是所谓的 pack(包) ,例如:

void print() { }

template <class T, class... Types>
void print(const T& firstArg, const Types&... args) {
	cout << firstArg << endl;
	print(args...);
}

auto

auto ite = find(c.begin(), c.end(), target);

ranged-base for

for (decl : coll){
	statement
}

虚指针、虚表

只要类里有虚函数,类的对象就有一个指针(vptr,指向一个虚函数表(虚表,vtbl。基类有虚函数,派生类必然有虚函数。

虚函数-虚指针-虚表.jpg

推荐阅读