【复习】C++面向对象基础3

文章目录

  • 面向对象基础3
    • 1.转换函数
    • non-explicit-one-argument constructor
    • pointer-like class,智能指针
    • Function-like class,仿函数
    • member template,成员模板
    • 模板特化及偏特化
    • 模板模板参数,template template parameter
    • variadic templates (c++11)可变个数的模板参数
    • auto (c++11)自动变量类型
    • ranged-base for (c++11) 简化的for循环
    • reference
    • 对象模型:虚指针vptr和虚表vtbl
    • Dynamic Binding,动态绑定与静态绑定
    • new、delete、malloc、free

面向对象基础3

1.转换函数

类型转换时调用的函数。

class Fraction{
public:
    Fraction(int num,int den=1):m_numberator(num), m_denominator(den){}
    
    operator double() const{
        return (double)(m_numerator/m_denominator);
    }

private:
    int m_numerator;   // 分子
    int m_denominator; // 分母
}

{
    Fraction f(3,5);
    double d = 4 + f;
    // 编译器遇到上面这句,它尝试看全局有没有一个这样的函数
    int operator+(int& a1, const Fraction& a2);
    // 即有没有全局的操作符重载函数复合这个4+f。这里没有
    // 它继续找,找到了Fraction中有个转换函数可以将f转成double数据,这样double d=4+f;也能正常运行。
}

上述就是将class转成其他类型。


non-explicit-one-argument constructor

只有一个参数(当只输入一个参数,就能构造时,即其他参数可以是默认参数)的构造函数,且是默认的(implicit),非explicit修饰的。即这个构造函数将会被在需要隐式转换时调用。

class Fraction{
public:
    Fraction(int num,int den=1):m_numberator(num), m_denominator(den){}
    
    Fraction operator+(const Fraction& f){
        return Fraction(....)
    }

private:
    int m_numerator;   // 分子
    int m_denominator; // 分母
}

{
    Fraction f(3,5);
    Fraction d2 = f + 4;
    // 首先编译器查看有没有操作符重载函数能使得这行代码正确运行,这里没有。
    // 接着这里编译器发现可以将4作为实参,调用Fraction(int num, int den=1)这个构造函数,隐式的将4转换成了一个Fraction对象,这样也能满足代码运行。
    // 当然不可缺少的是,上面的操作符重载函数operator+(const Fraction & f)
	
    // !!!!
    // 若此时构造函数用explicit修饰,表示,不允许隐式转换,必须显示调用,那么上述d2=f+4就会报错!
}

这种机制实际上是一种转换函数的反操作,隐含的类型转换。

要确保代码只有1条可调用的路径,当有两种以上方式时,编译器就不知道怎么调用函数了,就会出现ambiguous错误!


pointer-like class,智能指针

这里描述的是shared_ptr,将一个class做成一个指针一样的东西,指针能做的操作,这个class都可以。但它是class,可以由用户自定义一些功能,相当于增强指针。

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;
    long* pn;
    .....
};

struct Foo{
    ....
    void method(){...}
}

{
    // 使用
    shared_ptr<Foo> sp(new Foo);
    // 这个sp实际上是一个shared_ptr对象,但这里可以对他像指针那样取值(也叫解引用)以及调用函数操作。
    Foo f(*sp);
    sp->method();
    // 实际上内部是通过操作符重载实现的。
}

在STL中的应用:迭代器!


Function-like class,仿函数

将一个类设计的像一个函数,即可调用。重载括号调用操作符。标准库中仿函数都会继承一些特定的base class。


member template,成员模板

类的成员函数是个模板函数。

#pragma once
#include

namespace member_template {

// 鱼
class Fish{ };
// 鲤鱼
class Crap : public Fish{};

// 鸟类
class Bird {};
// 麻雀
class Sparrow : public Bird {};

// 构建一个模板类,表示一个数据对,数据类随意
template<class T1, class T2>
class Pair {
public:
	// 无参的构造函数,这里给first和second两个成员初始化。本文件编译的时候没问题,
	// 但是具体使用的时候还会编译,根据实际传入的T1和T2类型,保证T1和T2要具有无参构造函数。
	Pair():first(T1()),second(T2()) {}

	// 传参的构造函数,显然需要满足T1和T2需要实现拷贝构造,初始化列表调用的是拷贝构造
	Pair(const T1& t1, const T2& t2):first(t1),second(t2) {}

	// 成员模板函数,允许函数再传入泛型.
	template<class U1, class U2>
	// 注意:这里参数初始化列表有个自动的向上转型的过程。它要求p的first也就是U1类型必须是this.first,即T1的子类,否则转型会失败。
	Pair(const Pair<U1, U2>& p) : first(p.first), second(p.second) {}
	
	// 为了方便,这里没有访问约束
	T1 first;
	T2 second;
};

void test() {
	Pair<Crap, Sparrow> p;
	// 构建鱼-鸟数据对,传入的是鲤鱼-麻雀数据对,这是合理的
	Pair<Fish, Bird> p2(p); // 相当于Pair p(Pair())
	Pair<Crap, Sparrow> p3(p2); // error!无法从“const member_template::Bird”转换为“const member_template::Sparrow”(发生在31行,即成员模板函数!)
}

}

这里需要用户保证U1和U2是T1和T2的子类,并没有语法约束。相比Java的泛型类型上下限就比较直观了。

STL中的应用:shared_ptr

template<typename _Tp>
class shared_ptr:public __shared_ptr<_Tp>
{
	...
	template<typename _Tp1>
	explicit shared_ptr(_Tp1* __p):__shared_ptr<_Tp>(__p){}
	...
}

{
	// 使用
	Base1* ptr = new Derived1;// up-cast
	shared_ptr<Base1> sptr(new Derived1);
	// 用子类的指针包装成父类的智能指针
}

模板特化及偏特化

泛化:模板。泛,表示通用。

特化:specialization。

使用:为某个具体的类型,进行特化处理。同时可以匹配泛化与特化时,编译器先匹配特化

#pragma once
#include
#include

using namespace std;

namespace specialization {

template<typename T>
void print() {
	cout << "这里是泛化模板方法。。" << endl;
}

// 特化时,泛型类型固定了,所以不需要typename或者class T
template<>
void print<int>() {
	cout << "这里是为int提供的特化方法。。" << endl;
}

template<>
void print<string>() {
	cout << "这里是为string提供的特化方法。。" << endl;
}

void test() {
	print<float>();
	print<int>();
	print<string>();

	// result
	// 这里是泛化模板方法。。
	// 这里是为int提供的特化方法。。
	// 这里是为string提供的特化方法。。
}

}

泛化可以叫做全泛化,对应的特化, 又有偏特化。partial specialization。

  • 参数个数的“偏”
  • 参数类型范围的“偏”
// 参数个数的偏特化

// 这个vector是个模板类,需要传入一个一个类型T,以及,一包类型Alloc(这个Alloc代表了一系列类型的打包)
template<typename T, typename Alloc=...>
class vector
{
	...
}

// 将上述泛化模板中的T制定为bool,进行特化处理,这里的类型泛化只能按顺序从左到右,即,加入vector有多个泛型类型:A,B,C,D,E,那么不能特化A,C,E,然后让BD任然保持泛化。
template<typename Alloc=...>
class vector<bool, Alloc>
{
	...
}
// 参数范围的偏特化

// 泛化方法,所有类型都能作为泛型类型传入
template<typename T>
class C{
    ....
}

// 这里将类型缩小为只能是某个类型的指针
template<typename U>
class C<U*>{
    ...
}

模板模板参数,template template parameter

template<typename T, template<typename T> class Container>>
class XCls
{
private:
    Container<T> c;
public:
    ...
}
/*
这个XCls是个模板类,要求两个泛型参数,1,传入一个类型T;2,传入一个具有模板类型T的class,由于这里T类型不定,这个class也是不定的,所以也是一种泛型
*/

{
    // 使用
    template<typename T>
    using Lst = list<T, allocator<T>>;
    
    // 正确!XCls传入两个参数,string类型,以及以string为泛型类型的list
    XCls<string,Lst> mylst;
    // 错误!这个XCls的list泛型类型不符合XCls的声明
    XCls<string, list> mylst2;
}

这里实现的效果常用于容器的嵌套。


variadic templates (c++11)可变个数的模板参数

#pragma once
#include
#include

using namespace std;

namespace varicdic_tmp{

	// 注意:这个空函数必须存在!下面的print递归调用到最后一层时,t2为空,即只有1个参数了(t1),
	// 那么会重载调用到这个空参数的print,否则错误!
	void print(){}

	template<typename T, typename... Types>
	void print(const T& t1, const Types&... t2 ) {
		cout << typeid(t1).name() << "," << t1 << endl;
		// 这里是个递归调用,参数t1在上面打印出来
		// 之后的参数又调用本函数,由于print函数的第二个参数是可变参数,所以递归调用时会自动将t2拆分成两部分以满足参数要求
		print(t2...);
	}

	void test() {
		print(7.5, "hello", 42, bitset<16>(999));
	}

	// result:
	// double, 7.5
	// char const[6], hello
	// int, 42
	// class std::bitset<16>, 0000001111100111
}

上述例子三个注意点:

  • 可变数量模板参数的定义方法typename... Types
  • 可变数量模板参数的使用方法args...,实参名+三个点,这里实参名是“一包”参数
  • 考虑递归推出条件。可在print中做判空,也可像上面那样对print做重载来应对递归跳出条件。

auto (c++11)自动变量类型

实际就是根据赋值的右边结果自动推断左边定义参数的类型,所以必须时同时定义和推断变量,不能单独创建auto变量,而不给右边的推断。

list<stirng> c;
...
list<string>::iterator ite;
ite = find(c.begin(), c.end(), target);
// 显然由于c的泛型类型是string,编译器已知,所以ite的类型实际可以从find函数的返回值类型推断出来。所以可以简化为下面的写法

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

// 但是auto变量不能分步创建,必须定义就赋值,才能推断出类型.下面这样就会错误!!!!
auto ite; // error!!!
ite = find(c.begin(), c.end(), target);

有点类似于Java中的泛型类型自动推断,常用于容器的操作中。通常在创建容器时往往已经确定传入的模板参数。这就允许用户使用auto变量。


ranged-base for (c++11) 简化的for循环

// 即:decl为元素,coll待遍历的容器
for(decl : coll){
    statement
}

// c++2.0允许直接使用{}来定义一个类似python的tuple元组类型。实际就是一组数据,可以看作数组。
for(int i : {2,3,4,5,6}){
    cout << i << endl;
}

// 当然结合auto变量可以实现最简单
vector<double> vec;
...
// 这里是值传递,对elem操作不影响vec中的元素
for (auto elem : vec){
    cout << elem << endl;
}
// 这里是引用操作,会导致vec中所有元素扩大3倍
for (auto& elem : vec){
    elem *= 3;
}

reference

引用是对原变量的一个别名,但内部是通过指针实现的。

  • 引用变量必须设初值。
  • 引用变量不能更换引用的对象。

为了满足逻辑上引用和被引用变量是同一个“东西”,编译器为引用制造了一些“假象”:

int x=0;
int& r = x; // 必须设初值
int x2=5;

// r不能更改引用对象
// 所以这里不是将r引用到x2,而是x和r都等于5.
r = x2;

// 虽然r底层是指针,32位机指针变量4字节,但是为了保持引用r和被引用对象x逻辑相同,r的sizeof大小和x一致,即,若此处x是1000字节的对象,那么r也是1000字节
sizeof(r) == sizeof(x);
// 同理,引用也保持地址上的逻辑一致性
&x == &r;

很少直接声明引用类型的变量,通常引用作为参数传递的方式使用。

传引用参数的好处:

  • 内部相当于是指针传递,速度快。但注意的是对参数的操作都是inplace的操作,没有发生额外拷贝,是在原实参上进行修改。
  • 采用引用参数类型,使得调用端和函数内部代码一致,体现了引用就是被引用对象的别名,他们逻辑一致的思想。
  • 同时,引用类型与原类型两种参数类型的函数会被认为是相同的函数,不能作为函数重载条件(不是签名的一部分)。(函数的const类型,即class的const函数的const可以作为重载条件,即方法的const是函数签名的一部分)

对象模型:虚指针vptr和虚表vtbl

若class存在虚函数,则class会在数据起始增加一个vptr,虚函数指针,指向一个虚函数表vtbl,该表存放所有虚函数的地址。

子类继承父类的数据对象,但是不继承vptr,而是拥有自己的vptr。

(更多的内容之前已经有详细介绍)


Dynamic Binding,动态绑定与静态绑定

动态绑定,即函数的调用是一种动态过程,表现为多态的特性。内部实现是vptr和vtbl与虚函数。

#pragma once
#include
using namespace std;
namespace dynamic_binding {

class A {
private:
	int a, b;

public:
	A():a(1),b(2){}

	virtual void vfunc1() {
		cout << "A's vfunc1!" << endl;
	}
	void func2() {
		cout << "A's func2!" << endl;
	}

};

class B : public A {
private:
	int a, b;

public:
	B() :a(1), b(2) {}

	virtual void vfunc1() {
		cout << "B's vfunc1!" << endl;
	}
	void func2() {
		cout << "B's func2!" << endl;
	}
};

void test() {
	B b;
	A a = (A)b;
	// 这里发生强转,调用的都是A的方法,此处是静态绑定。
	a.func2();  // A's func2!
	a.vfunc1(); // A's vfunc1!
	
	A* c = new B;
	c->func2();	 // A's func2!
	// 发生动态调用(多态)。条件:1)指针。2)父子类指针自动向上转型。3)虚函数
	c->vfunc1(); // B's vfunc1!
}

}


new、delete、malloc、free

  • new、delete都是操作符。
  • new是先分配memory,再调用constructor
  • delete是先析构,再释放memory。
  • new成功时返回对象类型的指针。而malloc返回void类型指针,需要强转。
  • new失败,会抛出bac_alloc异常,而不会返回null;malloc失败返回null。通常在C中,用malloc会进行null判断,而在C++中用new应该采用try catch异常处理。或者使用安全的new,即抑制异常的new.int* p = new(std::nothrow) int;//此时失败会返回空指针

你可能感兴趣的:(C++修炼)