1. 目标
- 泛型编程(Generic Programming)和面向对象编程(Object-Oriented Programming)虽然分属不同思维,但它们正是C++的技术主线,所以本课程也讨论template(模板)。
深入探索面向对象之继承关系(inheritance)所形成的对象模型(Object Model),包括隐藏于底层的this指针,vptr(虚指针),vtbl(虚表),virtual mechanism(虚机制),以及虚函数(virtual functions)造成的polymorphism(多态)效果。
2.转换函数conversion function
2.1 operator double() const {}
class Fraction
{
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) { }
operator double() const { // 转换函数:无参数;返回类型:防止写矛盾,所以不用写
return ((double)m_numerator)/m_denominator;
}
private:
int m_numerator; // 分子
int m_denominator; // 分母
}
转换函数不可以有参数,也不需要返回值。通常都会是const.
2.2 non-explicit one argument ctor
class Fraction { public: Fraction(int num, int den=1)//没有explicit,一个参数,的构造函数 : m_numerator(num), m_denominator(den) {} Fraction operation + (const Fraction& f){ return Fraction(......); } private: int m_numrator; int m_denominator; };
Fraction f(3, 5); Fraction d2 = f + 4;//调用non-explicit cotr将4转为Fraction(4),然后调用operator+
2.3 conversion function vs. non-explicit one argument ctor
Fraction f(3, 5); Fraction d2 = f + 4;//[Error] ambiguous
/编译器方法1:先找全局函数+,第一个参数是整数或者浮点数,第二个参数是Fraction → ng. 可以将4转换为Fraction,发现有non-explicit-one-argument构造函数,行的通
- 编译器方法2:找Fraction转换成double的方法 → ok. 将f转换为double,发现有转换函数,也行的通
2.4 explicit-one-argument ctor
class Fraction
{
pulbic:
explict Fraction(int num, int den=1)//explict表示明确的.
//告诉编译器,既然设计成构造函数,就应该以构造函数的形式来用;告诉编译器不要自动帮我做事情,等我真的调用构造函数的时候你再调用
//不可以自动将4,变成1/4;
: m_numerator(num), m_denominator(den) {}
operator double() const{
return (double) (m_numerator / m_denominator);
}
Fraction operator+(const Fraction& f){
return Fraction(......);
}
private:
int m_numerator;
int m_denominator;
};
Fraction f(3, 5); Fraction d2 = f + 4;
//[Error]conversion from ‘double’ to ‘Fraction’ requested
explicit这个关键字,90%用在构造函数前面。
2.5 STL中实例
3. 智能指针与仿函数
3.1 pointer-like classes,关于智能指针
pointer-like classes中一定有一个指向某个类的对象的指针
template<class T> class shared_ptr { public: T& operator*() const//操作符*重载 { return *px;} T* operator->() const//操作符->重载 { return px;} shared_ptr(T* p) : px(p) {} private: T* px;//px是一个指针 long* pn; ...... };
3.2 function-like classes,所谓仿函数
对操作符()的重载
4. namespace
5. template
5.1 class template,类模板
5.2 function template,函数模板
类模板使用时要指定类的类型,函数模板使用时不用指定,因为函数模板在使用时,一定是在调用它,调用时会放参数,编译器会对函数模板进行实参推导。
模板的编译,编出来也只是一个半成品,不能保证后面一定会成功,即后面在使用时,会在编译一次。
5.3 member template, 成员模板
黄色部分的构造函数pair是一个成员模板。
Derived1—>Base1(鲫鱼—>鱼类)Derived2—>Base2(麻雀—>鸟类)
可以将一个由鲫鱼和麻雀构成的pair,放进(拷贝到)一个由鱼类和鸟类构成的pair中;反之不行。在标准库中大量的类的构造函数写成成员模板的形式,就是为了让构造函数更有弹性。
6. specialization, 模板特化
6.1 模板特化
范化就是模板,是特化的反面 特化是对某种特殊类型的处理
hash
6.2 模板偏特化 partial specialization
- 个数上
注意:特化的参数,需要从左边到右边连续的,比如有五个模板参数,不能特化1 3 5参数,留下2 4 参数
- 范围上
范围缩小成指针
-
6.3 template template paremeter 模板模板参数
XClsmylst2可以看出,Lst还是模板,类型是不确定的。
只有在 template<>中,class和typename可以共通 。//C++2.0的语法 template<typename T> using Lst = list<T, allocator<T>>;
这不是template template parameter
在使用时,如果要写出第二参数,写法:stack
7. C++标准库
8. C++11 三个主题
8.1 variadic templates
数量不定的模板参数
#include <iostream>
#include <bitset>
using namespace std;
void print() {
cout << __cplusplus << endl;
}
template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
cout << "firstArg = " << firstArg << " sizeof...(args) = " << sizeof...(args)<< endl;
print(args...); // 有种递归的感觉,但是每次递归的参数类型是变化的
}
int main() {
print(7.5, "hello", bitset<16>(377), 42); //bitset中应该重载<<
return 0;
}
sizeof…(args)可以算出pack中有几个参数。
标准库中很多代码都用这个语法重写了。
8.2 auto
不赋值的时候不能用auto,编译器无法推断类型。
初学者最好少用auto,要自己清楚自己每个变量的类型。
8.3 for range循环
9. reference
9.1 概念
引用:变量或对象的别名。
1:声明引用时必须显式初始化赋值,且设定初值后不可以再改变引用所代表的变量或对象,但可以修改其值。
2:引用与指针不等价,但大多数编译器底层使用指针实现引用。
3:变量或对象和其引用的内存大小相同、地址相同。(均为假象)
4:Java中,所有变量均为引用(reference)
9.2 常见用途
常用于参数传递(函数参数类型和返回类型的表示)
- 同名函数的签名,形参列表的非引用版本和引用版本不可同时存在,不构成函数重载。否则存在歧义,编译器不清楚应该调用哪个函数。
- const是函数签名的一部分,未使用const的版本和使用const的版本可同时存在,构成函数重载。所以const可以用来区分两个函数。
const 表示该成员函数不可以修改成员数据,所以该函数隐形的this指针会变成底层const(指向常量的指针:代表不能改变其指向内容的指针)
double imag ( T const * this, const double& im) {};
这样一来,如果实参是const 那就只能调用const成员函数;
而&相当于顶层const(常量指针,不可以改变指向)
10. 继承与组合关系下的构造和析构
10.1 继承关系下
- 继承关系:从内存/数据的角度而言,子类对象包含父类的成分。
- 构造函数的调用:由内向外
子类/派生类的构造函数先调用父类/基类的默认构造函数(由编译器完成),再执行本身。
Derived::Devired(…) : Base() { … } // 编译器自动调用父类默认构造
- 析构函数的调用:由外向内
子类/派生类的的析构函数先执行本身,再调用父类/基类的析构函数(由编译器完成)。
Derived::~Devired(…) { … ~Base() } // 执行完子类析构后,再执行父类析构
父类/基类的构造函数必须为虚函数,由子类进行重写,否则会导致未定义的行为 undefine behavior。
10.2 组合关系下
组合/复合关系:从内存/数据的角度而言,Container对象包含Component类的成分。
- 构造函数的调用:由内向外
Container的构造函数先调用Component的默认构造函数(由编译器完成),再执行本身。
Container::Container(…) : Component() { … } // 编译器自动调用Component类默认构造
- 析构函数的调用:由外向内
Container::~Container(…) { … ~Component() } // 执行完Container类析构后,再执行Component类析构
10.3 继承和组合关系下
- 从内存/数据的角度而言,Devired对象包含父类和Component类的成分
- 构造函数的调用:由内向外
Devired的ctor先调用Base的默认构造函数,再调用Component的默认构造函数,最后执行本身。
Devired::Devired(…) : Base(), Component() { … }
- 析构函数的调用:由外向内
Devired的析构函数先执行本身,再调用Component的析构函数,最后调用Base的析构函数。
Devired::~Devired(…) { … ~Component(), ~Base() }
11. 对象模型(Object Model):虚指针vptr和虚表vtbl
11.1 定义
虚指针(virtual pointer):当类包含虚函数时,其对象包含指向虚函数表的虚指针。
虚函数表/虚表(virtual function table):存储的元素均为函数指针,指向虚函数的内存地址。
注:继承虚函数,是继承函数的调用权,而不是函数所占的内存大小。
只要类中有虚函数,类的对象里面就多一个指针;
动态绑定的过程:指向对象的指针,依次通过虚指针、虚表来调用虚函数。
11.2 静态绑定(C语言) vs. 动态绑定(C++语言)
- 静态绑定(C语言):编译器针对调用的动作,编译为CALL_XXX(地址),当调用某个函数时,编译器进行解析并跳转至对应地址,随后return返回。 通过对象。
动态绑定(C++语言):通过指向对象的指针(可向上转型,即多态),依次查找虚指针、虚函数表及指向的虚函数地址。指向当前对象的指针即this指针。
11.3 虚机制
满足动态绑定的3个条件时,编译器会以动态绑定的形式进行编译。
通过指向对象的指针调用;
- 指向对象的指针向上转型(up-cast),即多态(polymorphism);
- 调用虚函数。
使用C语言的语法,实现的动态绑定:
((p->vptr)[n])(p);或( p->vptr[n])(p);
注:1. 具体调用哪个类的虚函数,由传入对象的具体类型决定。
2. 多态、虚函数、动态绑定(虚指针与虚函数表)为同一概念。
示例:存储不同形状的容器
- 不同形状对应的类,其对象所占用的内存大小可能不同。
- C++容器只能存储相同类型的元素。
为保证容器可以存储不同形状的对象,需将容器的元素类型定义为指向形状基类的指针类型。
例如:list
11.4. 对象模型(Object Model):this指针
this指针:当前调用成员变量或成员函数的对象的地址。
注1:C++中所有成员函数均包含隐藏的this指针,表示当前调用该成员函数的对象。
注2:this指针即指向当前对象的指针,可通过动态绑定,依次查找虚指针、虚表及指向的虚函数地址。
虚函数应用的场景:
- 多态(Polymorphism)
- 模板方法(Template Method):在抽象基类中定义整个算法流程,而将若干具体实现步骤(通过使用虚函数)延迟到子类中。模板方法使得子类可以不改变抽象基类的算法流程,即可重定义该算法的某些特定步骤(重写虚函数)。不同的子类能够以不同的逻辑实现特定步骤,而不影响抽象基类的算法流程。
12. const
常成员函数:const修饰的成员函数,只访问且不修改类的数据成员。
语法:T func() const {}
当成员函数的const版本和non-const版本同时存在时(即函数重载):
- 常对象(const object)只会(只能)调用成员函数的const版本;
- 非常对象(non-const object)只会(只能)调用成员函数的non-const版本。
注:非常成员函数(non-const)可调用常成员函数(const),反之不行。否则报错:connot convert ‘this’ pointer from ‘const class X’ to ‘class X &’. Conversion loses qualifiers.(转换丢失限定符)
示例:
template class std::basic_string<..> 包括2个同名成员函数。
STL字符串类通过引用计数实现,对象拷贝后共享同一份数据内容。
当涉及共享操作时,需考虑数据修改的问题。
/* const版本 */
// 常量字符串调用const版本,无需考虑COW
charT operator[](size_type pos) const
{
/* 不必考虑COW(Copy On Write) */
}
/* 非const版本 */
// 非常量字符串调用非const版本,需考虑COW
reference operator[](size_type pos)
{
/* 必须考虑COW(Copy On Write) */
}
注:const属于函数签名的一部分,使用和未使用const的同名函数构成函数重载。
13. new 和delete的补充
有三种new: plain new、array new、placement new。
13.1 new表达式和delete表达式
new表达式:先分配内存,再调用构造函数。
Complex* pc = new Complex(1, 2);
/* 编译器内部实现 */
Complex* pc;
//1.分配内存
//new操作符内部调用malloc()函数
void* mem = operator new(sizeof(Complex));
//2.静态类型转换
pc = static_cast<Complex*>(mem);
//3.调用构造函数
//pc调用构造函数,则pc即隐藏的this指针
pc->Complex::Complex(1, 2);
delete表达式:先调用析构函数,再释放内存。
String* ps = new String("Hello");
...
delete ps;
/* 编译器内部实现 */
//1.调用析构函数
String::~String(ps);
//2.释放内存
//delete操作符内部调用free(ps)函数
operator delete(ps);
其中 operator new()和operator delete()函数可重载。
13.2 重载全局函数::operator new、::operator delete、::operator new[]、::operator delete[]
重载后的全局函数由编译器调用。
void* myAlloc(size_t size)
{
return malloc(size);
}
void myFree(void* ptr)
{
free(ptr);
}
/* 重载全局函数::operator new */
inline void* operator new(size_t size)
{
cout << "self global new()" << endl;
return myAlloc(size);
}
/* 重载全局函数::operator new[] */
inline void* operator new[](size_t size)
{
cout << "self global new[]()" << endl;
return myAlloc(size);
}
/* 重载全局函数::operator delete */
inline void operator delete(void* ptr)
{
cout << "self global delete()" << endl;
myFree(ptr);
}
/* 重载全局函数::operator delete[] */
inline void operator delete[](void* ptr)
{
cout << "self global delete[]()" << endl;
myFree(ptr);
}
13.3 重载成员函数operator new/delete、operator new[]/delete[]
重载成员函数operator new/delete、operator new[]/delete[],可改变调用者的行为。
class Foo {
public:
/* per-class allocator */
void* operator new(size_t);
void operator delete(void*, size_t /* optional */);
};
/* 调用成员函数operator new */
Foo* p = new Foo;
/* 调用成员函数operator new的底层步骤 */
try{
// 1.调用重载后的成员函数operator new
void* mem = operator new(sizeof(Foo));
// 2.静态类型转换:void* → Foo*
p = static_cast<Foo*>(mem);
// 3.调用构造函数
p->Foo:Foo();
}
/* 调用成员函数operator delete */
delete p;
/* 调用成员函数operator delete的底层步骤 */
// 1.调用析构函数
p->Foo:~Foo();
// 2.调用重载后的成员函数operator delete
operator delete(p);
重载成员函数operator new[]/delete[]
class Foo {
public:
/* per-class allocator */
void* operator new[](size_t);
void operator delete[](void*, size_t /* optional */);
};
/* 调用成员函数operator new[] */
Foo* p = new Foo[N];
/* 调用成员函数operator new[]的底层步骤 */
try{
// 1.调用重载后的成员函数operator new[]
void* mem = operator new(sizeof(Foo) * N + 4);
// 2.静态类型转换:void* → Foo*
p = static_cast<Foo*>(mem);
// 3.调用构造函数
p->Foo:Foo(); /* 调用N次构造函数 */
}
/* 调用成员函数operator delete[] */
delete[] p;
/* 调用成员函数operator delete[]的底层步骤 */
// 1.调用析构函数
p->Foo:~Foo(); /* 调用N次析构函数 */
// 2.调用重载后的成员函数operator delete
operator delete(p);
13.4 示例
示例1:调用全局函数及成员函数的operator new/delete
若调用者使用全局函数::operator new/delete,则编译器底层调用全局的void ::operator new(size_t)或void operator delete(void)。
示例2:调用重载的成员函数operator new[]/delete[]
示例3 调用重载的全局函数::operator new[]/delete[]
13.5 重载placement new / placement delete,即new() delete()
类成员函数operator new()可进行函数重载,需满足如下条件:
- 每个重载版本的函数声明,必须有唯一的参数列表(不同重载版本的参数列表不能相同);
- 参数列表的第1个参数,必须是size_t类型;
- 参数列表的其余参数,以new所指定的placement argument为初值,即出现在new(…)小括号中的参数。
类成员函数operator delete()可进行函数重载(非强制),但重载版本一定不会被delete所调用。仅当new所调用的构造函数抛出异常(Exception),才会调用重载版本的operator delete()。
主要作用是释放未完全创建成功的对象所占用的内存。
注:operator delete()即使未与operator new()一一对应,也不会出现任何报错,编译器会视为:放弃处理构造函数抛出的异常。