C++ Part2 兼谈对象模型合集

 

1 转换

1.1 转换函数

将当前对象的类型转换成其他类型

  • operator 开头,函数名称为需要转成的类型,无参数
  • 前面不需要写返回类型,编译器会自动根据函数名称进行补充
  • 转换函数中,分子分母都没改变,所以通常加 const
// class Fraction里的一个成员函数
operator double() const
{
    return (double) (m_numerator / m_denominator);
}
Fraction f(3,5);
double d = 4 + f; //编译器自动调用转换函数将f转换为0.6

1.2 non-explicit-one-argument ctor

将其他类型的对象转换为当前类型

one-argument 表示只要一个实参就够了

// non-explicit-one-argument ctor
Fraction(int num, int den = 1) 
    : m_numerator(num), m_denominator(den) {}
Fraction f(3,5);
Fraction d = f + 4; //编译器调用ctor将4转化为Fraction

1.3 explicit

当上面两个都有转换功能的函数在一起,编译器调用时都可以用,报错

class Fraction
{
public:
	Fraction(int num, int den = 1) 
		: m_numerator(num), m_denominator(den) {}
	operator double() const
	{
		return (double)m_numerator / m_denominator;
	}
	Fraction operator+(const Fraction& f) const
	{
		return Fraction(...);
	}
private:
	int m_numerator; // 分子
	int m_denominator; // 分母
};
...
    
Fraction f(3,5);
Fraction d = f + 4; // [Error] ambiguous

one-argument ctor 加上 explicit,表示这个 ctor 只能在构造的时候使用,编译器不能拿来进行类型转换了

...
explicit Fraction(int num, int den = 1) 
    : m_numerator(num), m_denominator(den) {}
...
    
Fraction f(3,5);
Fraction d = f + 4; // [Error] 4不能从‘double’转化为‘Fraction’

关键字 explicit 主要就在这里运用

2 xxx-like classes

2.1 pointer-like classes

2.1.1 智能指针

  • 设计得像指针class,能有更多的功能,包着一个普通指针
  • 指针允许的动作,这个类也要有,其中 *-> 一般都要重载
template <typename T>
class shared_ptr
{
public:
	T& operator*() const { return *px; }
	T* operator->() const { return px; }
	shared_ptr(T* p) : ptr(p) {}
private:
	T* px;
	long* pn;
};

在使用时,*shared_ptr1 就返回 *px

但是 shared_ptr1-> 得到的东西会继续用 -> 作用上去,相当于这个->符号用了两次

image-20230807095542200

2.1.2 迭代器

以标准库中的链表迭代器为例,这种智能指针还需要处理 ++ -- 等符号

node 是迭代器包着的一个真正的指针,其指向 _list_node

image-20230807100734372

  • 下图 *ite 的意图是取 data——即一个 Foo 类型的 object
  • 下图 ite->method 的意图是调用 Foo 中的函数 method

image-20230807100804223

2.2 function-like classes

设计一个class,行为像一个函数

函数行为即 —— xxx() 有一个小括号,所以函数中要有对 `()` 进行重载

template <class pair>
struct select1st ... // 这里是继承奇特的Base classes,先不管
{
	const typename pair::first_type& // 返回值类型,先不管
	operator()(const pair& x) const
	{
		return x.first;
	}
};

...
//像一个函数一样在用这个类
select1st<my_pair> selector;
first_type first_element = selector(example_pair);

//还可以这样写,第一个()在创建临时对象
first_type first_element = select1st<my_pair>()(example_pair);

...

3 模板

3.1 类模板/函数模板

补充:只有模板的尖括号中<>,关键字 typenameclass 是一样的

3.2 成员模板

它即是模板的一部分,自己又是模板,则称为成员模板

其经常用于构造函数

  1. ctor1 这是默认构造函数的实现;它初始化 firstsecond 分别为 `T1` 和 `T2` 类型的默认构造函数生成的默认值
  2. ctor2 这是带参数的构造函数的实现;它接受两个参数 ab,并将它们分别用来初始化 firstsecond 成员变量
  3. ctor3 这是一个==模板构造函数==,接受一个不同类型的 `pair` 对象作为参数;它允许从一个不同类型的 pair 对象构造当前类型的 pair 对象,在构造过程中,它将源 pair 对象的 firstsecond 成员变量分别赋值给当前对象的成员变量,使其具有一定的灵活性和通用性
template <class T1, class T2>
struct pair
{
	T1 first;
	T2 second;
	pair() : first(T1()), second(T2()) {} //ctor1
	pair(const T1& a, const T2& b) : 	  //ctor2
		first(a), second(b) {}

	template <class U1, class U2>		  //ctor3
	pair(const pair<U1, U2>& p) : 
		first(p.first), second(p.second) {}
};
  • 例一,可以使用 <鲫鱼,麻雀> 对象来构造一个 <鱼类,鸟类> 的pair

    image-20230807152238567

  • 例二,父类指针是可以指向子类的,叫做 up-cast;智能指针也必须可以,所以其构造函数需要为==模板构造函数==

    image-20230807152501305

3.3 模板模板参数

即模板中的一个模板参数也为模板,下图黄色高亮部分

image-20230807161321152

  • XCLs<string, list> mylist 中即表示:容器 liststring 类型的—— 创建一个 string 的链表;Container<T> c; 即表示 list<srting> c;

  • 但是这样 Container<T> c; 语法过不了,容器 list 后面还有参数,需要用中间框和下面框下一行的代码 —— c++11的内容

注:下面不是模板模板参数

image-20230807162712548

class Sequence = deque<T> 是有一个初始值,当没指定时就初始为 deque<T>

在要指定时,如最后一行中的 list<int> 是确切的,不是模板

4 specialization 特化

4.1 全特化 full specialization

模板是泛化,特化是泛化的反面,可以针对不同的类型,来设计不同的东西

  • 其语法为template<> struct xxx<type>
template<>
struct hash<char>
{
...
    size_t operator()(char& x) const {return x;}
};

template<>
struct hash<int>
{
...
	size_t operator()(int& x) const { return x; }
};
  • 这里编译器就会用 int 的那段代码;注意:hash<int>() 是创建临时变量
cout << hash<int>()(1000)

4.2 偏特化 partial specialization

4.2.1 个数上的偏

例如:第一个模板参数我想针对 bool 特别设计

image-20230807155256372

注意绑定模板参数不能跳着绑定,需要从左到右

4.2.2 范围上的偏

例如:想要当模板参数是指针时特别设计

image-20230807160122944

C<string> obj1; //编译器会调用上面的
C<string*> obj2; //编译器会调用下面的

5 三个C++11新特性

5.1 variadic templates

模板参数可变化,其语法为 ... (加在哪看情况)

// 当参数pack里没有东西了就调用这个基本函数结束输出
void print() {
}

// 用于打印多个参数的可变参数模板函数
template <typename T, typename... Args>
void print(const T& first, const Args&... args) {
    std::cout << first << " ";
    print(args...);  // 使用剩余参数进行递归调用
}

int main() {
    print(1, "Hello", 3.14, "World");
    return 0;
}

还可以使用 sizeof...(args) 来得到参数pack里的数量

5.2 auto

编译器通过赋值的返回值类型,自动匹配返回类型

image-20230808080207006

注:下面这样是不行的,第一行编译器找不到返回值类型

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

5.3 ranged-base for

for 循环的新语法,for(声明变量 : 容器),编译器会从容器中依次拿出数据赋值给声明变量中

for (decl : coll)
{
    statement
}

//例
for (int i : {1, 3, 4, 6, 8}) // {xx,xx,xx} 也是c++11的新特性
{
    cout << i << endl;
}

注意:改变原容器中的值需要 pass by reference

vector<double> vec;
...

for (auto elem : vec) //值传递
{
    cout << elem << endl;
}
for (auto& elem : vec) //引用传递
{
    elem *= 3;
}

6 多态 虚机制

6.1 虚机制

当类中有虚函数时(无论多少个),其就会多一个指针—— vptr 虚指针,其会指向一个 vtbl 虚函数表,而 vtbl 中有指针一一对应指向所有的虚函数

有三个类依次继承,其中A有两个虚函数 vfunc1() vfunc2(),B改写了A的 vfunc1(),C又改写了B的 vfunc1(),子类在继承中对于虚函数会通过指针的方式进行——因为可能其会被改写

继承中,子类要继承父类所有的数据和其函数调用权,但虚函数可能会被改写,所以调用虚函数是==动态绑定==的,通过指针 p 找到 vptr,找到vtbl,再找到调用的第n个虚函数函数——( *(p->vptr[n]) )(p)

image-20230808095746683

编译器在满足以下三个条件时就会做==动态绑定==:

  1. 通过指针调用
  2. 指针是向上转型 up-cast ——Base* basePtr = new Derived;
  3. 调用的是虚函数

编译器就会编译成 ( *(p->vptr[n]) )(p) 这样来调用

例如:用一个 Shape(父类)的指针,调用 Circle(子类)的 draw 函数(每个形状的 draw 都不一样,继承自 Shape)

==多态==:同样是 Shape 的指针,在链表中却指向了不同的类型

list<Shape*> Mylist

image-20230808104025485

多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护

6.2 动态绑定

image-20230808111646258

a.vfunc1() 是通过对象来调用,是 static binding 静态绑定

在汇编代码中,是通过 call 函数的固定地址来进行调用的

image-20230808112307107

pa 是指针,是向上转型,是用其调用虚函数—— dynamic binding 动态绑定

在汇编代码中,调用函数的时候,蓝框的操作用 c语言 的形式即是 —— ( *(p->vptr[n]) )(p)

下面同理

7 reference、const、new/delete

7.1 reference

x 是整数,占4字节;p 是指针占4字节(32位);`r` 代表`x`,那么`r`也是整数,占4字节

int x = 0;
int* p = &x; // 地址和指针是互通的
int& r = x; // 引用是代表x

引用与指针不同,只能代表一个变量,不能改变

引用底部的实现也是指针,但是注意 object 和它的 reference 的大小是相同的,地址也是相同的(是编译器制造的假象)

sizeof(r) == sizeof(x)
&x == &r

reference 通常不用于声明变量,用于参数类型和返回类型的描述

image-20230808091745371

以下 imag(const double& im)imag(const double im) 的签名signature 在C++中是视为相同的——二者不能同时存在

double imag(const double& im) /*const*/ {....}
double imag(const double im){....} //Ambiguity

注意:const 是函数签名的一部分,所以加上后是可以共存的

7.2 const

const 加在函数后面 —— 常量成员函数(成员函数才有):表示这个成员函数保证不改变 class 的 data

  const object non-const object
const member function(保证不改变 data members) ✔️ ✔️
non-const member function(不保证 data members 不变) ✔️

==COW==:Copy On Write

多个指针共享一个 “Hello”;但当a要改变内容时, 系统会单独复制一份出来给a来改,即 COW

image-20230801101907977

在常量成员函数中,数据不能被改变所以不需要COW;而非常量成员函数中数据就有可能被改变,需要COW

charT
operator[] (size_type pos)const
{
	.... /* 不必考虑COW */   
}

reference
operator[] (size_type pos)
{
    .... /* 必须考虑COW */
}

函数签名不包括返回类型但包括const,所以上面两个函数是共存的

当两个版本同时存在时,const object 只能调用 const 版本,non-const object 只能调用 non-const 版本

7.3 new delete

7.3.1 全局重载

  • 可以全局重载 operator newoperator deleteoperator new[]operator delete[]
  • 这几个函数是在 new 的时候,编译器的分解步骤中的函数,是给编译器调用的

注意这个影响非常大!

inline void* operator new(size_t size){....}
inline void* operator new[](size_t size){....}
inline void operator delete(void* ptr){....}
inline void operator delete[](void* ptr){....}

7.3.2 class中成员重载

  • 可以重载 class 中成员函数 operator newoperator deleteoperator new[]operator delete[]
  • 重载之后,new 这个类时,编译器会使用重载之后的
class Foo
{
public
    void* operator new(size_t size){....}
    void operator delete(void* ptr, size_t size){....} // size_t可有可无
    
    void* operator new[](size_t size){....}
    void operator delete[](void* ptr, size_t size){....} // size_t可有可无
    ....
}
// 这里优先调用 members,若无就调用 globals
Foo* pf = new Foo;
delete pf;

// 这里强制调用 globals
Foo* pf = ::new Foo;
::delete pf;

7.3.3 placement new delete

可以重载 class 成员函数 placement new operator new(),可以写出多个版本,前提是每一个版本的声明有独特的传入参数列,且其中第一个参数必须是 size_t,其余参数出现于 new(.....) 小括号内(即 placement arguments

Foo* pf = new(300, 'c') Foo; // 其中第一个参数size_t不用写
// 对应的operator new
void* operator new (size_t size, long extra, char init){....}

我们也可以重载对应的 class 成员函数 operator delete(),但其不会被delete调用,只当 new 调用的构造函数抛出异常 exception 的时候,才会调用来归还未能完全创建成功的 object 占用的内存