文章目录
- 面向对象基础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<iostream>
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<Base1,Base2> p(Pair<Derived1, Derived2>())
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<iostream>
#include<string>
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<iostream>
#include<bitset>
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<iostream>
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;//此时失败会返回空指针