转换函数(conversion function)
可以把"这种"东西,转化为"别种"东西。
即Fraction ——> double
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;//分母
};
Fraction f(3,5);
double d = 4 + f;
说明:
- 编译器先去查找是否有支持的函数能让这行代码通过。
- 是否有operator+(double,Fraction),重载了+号。
- 没有,则看能否将f转换为double。找到了operator double()const。
- 于是f变成了0.6。
std::cout << typeid(f).name()<<std::endl;//class Fraction
std::cout << d <<std::endl;//4.6
注意:
- 我在侯捷老师的视频中的发现了一个小问题。
- 如上图所示(double)的位置写的不对,应任意写到后两个变量的前面,如我上面类的代码所示。
non-explicit-one-argument-ctor
具有一个实参的构造函数()
可以把"别种"东西,转化为"这种"东西。
即double ——> Fraction
class Fraction
{
public:
Fraction(int num,int den = 1):
m_numerator(num),m_denominator(den){}
Fraction operator+(const Fraction& f){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;
说明:
- 可以看到在Fraction类中我们重载了+运算符,可以使两个Fraction对象进行相加。
- 但是我们下面进行调用的时候使用的是一个整数与一个Fraction对象进行相加。
- 此时调用的形式与我们的设计不同,于是编译器去看看能不能将4转换为Fraction,如果可以转换,则符合了我们的+重载。
- 于是调用我们的构造函数Fraction(int num,int den = 1),将4转换为Fraction,进行加法。
转换冲突
- 此时,我们将上面两个例子中的两个成员函数整合。
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){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;
- 编译器报错,不知道该如何是好。
- 是4——>Fraction,还是Fraction——>4。
- 产生歧义。
explicit-one-argument ctor
- 给构造函数添加explict关键字,此时"别种"东西无法转换为"这种"东西即Fraction对象。
- 不让编译器去暗度陈仓地偷偷调用构造函数。
- 只有真正需要构造的时候采取调用。
class Fraction
{
public:
explicit 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){
return Fraction(...);
}
private:
int m_numerator;
int m_denominator;
};
Fraction f(3,5);
double d = 4 + f;
说明:
此时4向Fraction转换失败。f被转成double进行计算,结果为4.6
explicit多数用在构造函数处,少数还有在模板处。
标准库应用
类指针类(pointer-like classes)
智能指针
- 这个class创建的对象像指针。
- 因为想要它比普通指针多做一些事情。
- 即智能指针。
注意:"->"这个符号很特别
- 例如说,上图右侧中的* sp中的,* 号,在使用后就会消失。
- 但是sp->method(),我们可以看到,调用sp->在右侧的类中,返回px,再往下看px->method(),会发现,这里其实少了一个->,这里就体现出这个符号的特殊性了,得到的东西会继续用箭头符号作用上去。
迭代器
- 在运算符上比智能指针需要重载更多运算符,处理更多功能。
- 有特别功能的智能指针。
- 主要用于遍历容器。
- 示例——标准库中的list迭代器
- foo即data
- 注意与上面智能指针重载运算符的对比。
说明:
- 左边方框中的内容等同于右边话蓝线的部分。
仿函数(function-like classes)
- 设计一个类,让它的行为像函数。
- 小括号操作符,就叫做函数调用操作符。
- 所以如果有一个东西可以接受小括号操作符,就把这个东西称作函数,或者是一个像函数的东西。
- 具体的相关继承问题详见STL库部分。
namespace经验之谈
- 分块开发,避免命名冲突。
模板(template)
类模板(class template)
- 定义类的时候将允许使用者任意指定的类型抽出来。
- 使用时需要进行类型的指定。
函数模板(function template)
- 使用不需要指定类型。
- 编译器会自动进行实参推导。
说明:
- 首先编译模板。
- 接着再次编译,判断stone类型的运算是否合法。
成员模板(member template)
- 也就是模板的嵌套,模板中有模板。
- 如下图黄色部分。
说明: 黄色这一块是当前模板的一个成员,同时它自己也是个模板。所以它就叫做成员模板。
- T1,T2可以变化,U1,U2也可以变化。
在STL标准库中会大量出现成员模板,先来一个小示例:
解释:
- 鲫鱼类继承自鱼类,麻雀类继承自鸟类。
- 使用鲫鱼和麻雀构成的pair,然后拷贝到到鱼类和鸟类构成的pair,这样是可以的。反之则不行。
- 允许或不允许限制的条件为: 下方代码中的构造函数。(父类指针可以指向子类对象)
- 这样,让
构造函数更有弹性
。
template<calss T1,class T2>
struct pair{
...
template<class U1,class U2>
pair(const pair<U1,U2>&p):first(p.first),second(p.second){}
};
- shard_ptr内的使用
- 同普通指针的,父类指针可以指向子类对象。
- 补充:C++ Upcast(向上造型)
- up-cast为向上构造
- down-cast为向下构造
模板特化(specialization)
全特化
- 泛化的反面就是特化
- 泛化(又叫全泛化)指的是
用的时候指定类型
。- 根据特定的类型进行特殊处理,类似于函数重载。
// 泛化
#include <iostream>
using namespace std;
// 泛化
template<class Key>
struct hash1{};
// 特化
template<>
struct hash1<char>{
size_t operator()(char x)const {
return x;
}
};
template<>
struct hash1<long>{
size_t operator()(long x)const {
return x;
}
};
int main(void){
// 调用
hash1<long>()(1000);// 构造一个hash的临时对象,传递参数1000,找到上面的特化long
return 0;
}
- 与全特化对应的是偏特化(局部特化)
偏特化
- 个数上的偏
- 从左边开始绑定,不能跳。
- 范围上的偏
- 例如,从接收任意范围T,到接收指针T*
模块模板参数(tempalte template parameter)
- 即,模板的参数又是一个模板
- 如上图所示,传递任意的容器与元素类型进行组合
- 其中第一个打岔的部分,光看语法上并没有问题,但是,实际上在我们定义容器的时候有多个默认参数,这样做是无法通过编译的。
- 但是第二个OK
这个不是模板模板参数
- 调用中我们使用第二种方法,指明第二模板参数,其实这个list< int >就已经不是模板了,已经指明了,即使它是用模板设计出来的东西。
- 但是已经绑定,写死,list中的元素类型为int;
- 注意与本小节第一张图对比。
- 所以temp<class T,class Sequence =
deque< T >
>第二个参数,不是模板模板参数。
可变参数模板(variadic templates-since)
- 模板可接收任意个参数,详见下方示例:
#include <iostream>
#include <bitset>
using namespace std;
void print(){}
// 分为一个和一包
template<typename T,typename... Types>
void print(const T& firstArg,const Types&...args) {
cout<<firstArg<<endl;
print(args...);// 调用,将这一包拆开
// 注意: 到最后变成0个的时候,将会调用上面的print()
cout<<sizeof...(args)<<endl;// 获得这一包中有几个元素
}
int main(void){
print(7.5,"hello",bitset<16>(377),42);
return 0;
}
自动类型推导(auto)
- 示例
list<string>c;
//...
list<string>::iterator ite;
ite = find(c.begin(),c.end(),target);
- 使用实例
auto ite = find(c.begin(),c.end(),target);// 定义使用时就赋值
- 错误使用
auto ite;// 编译器不能也无法知道这个ite是什么,无法进行推导
ite = find(c.begin(),c.end(),target);
更简洁的for(ranged-base for)
for(decl: coll){
statement
}
- 示例
vector<double>vec;
//...
// pass by value 传值
for(auto elem:vec){
cout<<elem<<endl;
}
// pass by reference 传引用——改变原来的东西
for(auto& elem:vec){
elem *=3;
}
引用(reference)
引用一定要设初值,且之后无法再代表其它值。
- 示例
int x = 0;
int* p = &x;// p指向x
int& r = x;// r代表x
int x2 = 5;
r = x2;// x r都为5,相当于将值5赋给x
int& r2 = r;
- 有趣的一点: 编译器制造出的假象
- 大小相同,地址也相同
sizeof(r) == sizeof(x);
&x == &r;
- referece 就是一种漂亮的pointer
- 多用于参数传递——传引用
- same signature——相同函数签名,二者无法并存
- 函数名和参数列表包括后面的const为signature(函数签名)
- const 是函数签名(signature)的一部分
- 编译器不知道你调用的是谁
double imag(const double& im){...}
double imag(const double im){...}
对象模型(Object Model)
继承下的构造与析构
- 构造——由内而外
- 子类构造时,会先执行父类的默认构造函数,编译器会默认加上并执行。
- 析构——由外而内
- 子类析构时,会先析构掉自己执行完后,然后指定父类的默认析构函数,同样由编译器添加并执行。
复合下的构造与析构
构造——由内而外
析构——由外而内
继承+复合下的构造与析构
- 构造——由内而外
- 但是此时内有两个,也许在不同编译器上的实现手法不同,可能会导致顺序不同。
- 析构——由外而内
- 同上,要注意的是,上面先构造的,会后析构。
虚指针与虚函数表(vptr & vtbl)
- 虚指针指向虚函数表,虚函数表中都是函数指针。
- 虚函数的调用&执行,如下图所示:
- 调用虚函数的过程为动态绑定——即多态,父类指针可以接收具体的子类对象,即根据具体是哪个子类,调用该虚函数具体的形式。
- 调用指针->向上转型(转为具体的子类)->调用虚函数
- 补充:
- 继承父类,函数,继承的是调用权。
- 父类的虚函数子类也一定要有。
- 父类和子类中可以出现同名的函数,但实际上不是同一个。
this pointer
- 类的成员函数中,默认会有一个this指针传递进来。由编译器自己处理。
补充
const
- 修饰成员函数——即放到成员函数参数列表后:
- 表明该成员函数不打算修改成员变量的值
- 让编译器帮忙把关,如果修改了,则无法通过编译。
- 常量对象不能调用非常量成员函数,反之,可以。
- 但是,
当成员函数的const版本和非const版本都存在,则常量对象只能调用const版本,非const对象只能调用非const版本。
- 能加const就加const
- const属于函数签名的一部分
- 示例: 标准库中的string,区分调用者的意图:
new & delete
- 三种new——参考: C++ new的三种面貌
- new (operator): 即关键字new,实际在堆中分配内存时,调用下面两个
- operator new: 用于申请堆内存空间,类似于C语言中的malloc()
- pleacement new: 即放置new,在人为指定的特定内存创建对象,是一个特殊的operator new,对其进行了重载。
- 调用new实际上被分解为三条语句——表达式行为不能被修改,也就是分解的这件事情不能被修改,但是分解下去调用的函数可以被重载
//调用
MyComplex *pc = new MyComplex(1,2);
void* temp = operator new(sizeof(MyComplex));// 分配内存-相当于调用malloc(n)
pc = static_cast<MyComplex*>(temp);//转型
pc->MyComplex::MyComplex(1,2);//调用构造函数
- delete实际上被分解为两条语句
delete pc;
Complex::~Complex(pc);
operator delete(pc);//(即 调用free)
重载::operatpr new,::operator delete,::operator new[],::operator delete[]
重载member operator new/delete
- 一个对象
重载member operator new[]/delete[]
- 数组
示例
- 若无成员函数就调用globals
- 也可以强制使用globals
- 这个多出来的4是一个计数器,数组中的元素个数(gnu c)
- 无论你是否重载,这个计数器都会存在。
重载pleacement new,pleacement delete
- 类的成员函数,可以重载多个版本,每一个版本都要有独一无二的参数列。
- 第一个参数必需为size_t——大小
- 其余参数为使用时()中指定的参数,例如下方示例中的300,‘c’
- 重载operator delete()后,绝对不会被delete调用,只有当new所调用的构造函数抛出异常,才会调用这些版本的operator delete()
- 不是必须的,可重载,可不重载
- 示例
Foo* pf = new(300,'c')Foo;
basic_string使用new
- 当我们想在分配内存的时候无声无息的多分配一些,可以借用下面的思想。