C++面向对象高级编程 part4
2017-11-06 12:43:00
item1. 导读
将C++视为一个语言联邦,包含四个次语言:
- C
- Object - Oriented C++
- Template C++
- STL
1. 操作符重载的多种用途
- 转化函数/类型转化
- poniter-like class
- function-like class
item2. Conversion function
转换函数作用是将当前定义的类型转换为其他类型。
1. 语法
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; // f转换为0.6
注意⚠️:
- 转换函数没有参数。
- 转换函数不用声明返回值类型。
- 转换不应该改变被转化对象的数据。所以要加const声明。
2. 转换何时发生
- 显示转换
- 隐式转换:编译器在编译阶段自动查找匹配的转换形式。
item3. Non-explicit one argument constructor
one argument 是指一个实参。Non-explicit one argument constructor是指该构造函数可以接受一个实参,但不限形参的个数。
1. 语法
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; // 调用Non-explicit one argument constructor 将4 转换为Fraction(4,1)
Non-explicit one argument constructor 副作用
C++ primer 4th 393
可以用单个实参调用的构造函数,定义了从形参类型到该类类型的一个隐式转换。
引出的问题
潜在的隐式转换引入ambiguous/歧义,造成编译器无法解释代码。
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; //error, ambiguous
2. explicit关键字
- explict关键字限制编译器隐式的通过Non-explicit one argument constructor 将其他类型转换成被类型。
- explicit 常用于修饰构造函数,只需要在声明处使用,无需再定义处重复。
语法
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);
Fraction d = 4+f; // error, conversion from double to fraction
// 首先f转换为double ,然后 4+f 为double,
// 由于explict限定,单个double值无法转化为fraction
explict 仅限制隐式转换,不限制构造函数参数个数
#include <iostream>
class Test {
public:
explicit Test(int a, int b =1){}
};
int main() {
double a;
Test t(a); // ok
Test t2 = 1; // error
return 0;
}
item4. Pointer-like classes
为什么要设计pointer-like class: 设计比普通指针功能更多的指针,智能指针。
1. 智能指针
智能指针是一种pointer-like class。
语法
template <class T>
class Shared_ptr {
public:
T& operator *() {
return *ptr_;
}
T* operator->() {
return ptr_;
}
Shared_ptr(T* ptr) : ptr_(ptr) {}
private:
T* ptr_;
};
如何使class pointer-like :
- 重载操作符->
- 重载操作符*
operator ->的问题
....
T* operator->() const {
return px;
}
....
shared_ptr<Foo> sp (new Foo);
sp->method();
如果是直接调用形式:
sp->method();
等价于:
pxmethod()
但实际等价于:
px->method()
Why?
->操作符的作用在返回后会持续作用下去。
2. 迭代器
迭代器也是pointer-like class。
template <class T>
struct __list_node{
void* prev;
void* next;
T data;
};
template <class T, class Ref, class Ptr>
struct __list_itertor{
typedef __list_itertor<T,Ref,Ptr> self;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
link_type node;
reference operator*() const {
return (*node).data;
}
pointer operator->() const{
return &(operator*());
}
self& operator++()const{...}
self& operator--()const{...}
};
迭代器重载了更多的的操作符号。
item5. Function-like classes
1. 语法
template <class T1, class T2>
struct pair{
T1 first;
T2 second;
pair():first(T1()),second(T2()){}
pair(const T1& a, const T2& b) : first(a), second(b){}
};
template <class T>
struct identity {
const T& operator()(const T& x) const {return x;}
};
template <class Pair>
struct select1st{
const typename Pair::first_type&
operator()(const Pair&x) const{
return x.first;
}
};
template <class Pair>
struct select2nd{
const typename Pair::first_type&
operator()(const Pair& x) const{
return x.second;
}
};
- 需要重载operator();
- 重载函数可以有入参和返回值。
构造函数与函数对象/operator()的差别:构造函数调用时是
类型名()
,函数对象调用时是对象名()
不含数据成员的空类大小:理论是0,实现是1。
item6. Namespace
目的:防止名称冲突
item7. Class Template
item8. Function Template
1. 实参推导
编译器会在模版函数调用时,对实参进行参数推导。
- 类模版在创建对象时需要指明类型,函数模版不必。why?
声明类模版对象时,如果不提供模版参数类型,编译器无法获知该模版参数类型,所以不知道该创建什么对象。函数模版在调用时一定会指定参数(这时参数类型会被确认)。
item9. Member Template
1. 语法
template <class T1, class T2>
struct pair{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair():first(T1()),second(T2()){}
pair(const T1& a, const T2& b) : first(a), second(b){}
template <class U1, class U2>
pair(const pair<U1, U2>&p): first(p.first), second(p.second) {} // member template
};
class Base1{};
class Derived1: public Base1{};
class Base2{};
class Derived2: public Base2{};
- 成员模版,模版参数作用在成员函数中定义。
- 类模版,模版参数作用在成员变量中。
- 成员模版在类模版的基础上增加了模版的灵活性。
成员模版常被应用在标准库的构造函数中。
理解:member template 是 class template 中的 function template
2. 标准库中的应用
注意⚠️
应用场景:如何给模版类中的模版参数类型的成员,通过函数传递给其不同于该成员模版参数类型的实参(一般模版类中的模版参数类型是base class,参数是derived class,但实际测试即使不使用member template base/derived直接赋值也是ok的)?
class base {};
class derived : public base {};
void func1(base * b){
cout<<__func__<<endl;
}
template <class T>
class Ttype{
private:
T* base_;
public:
Ttype(T* base):base_(base) {
cout<<"created"<<endl;
}
};
int main() {
func(int(4));
derived* pderived;
func1(pderived);
Ttype<base> b(pderived); // ok
return 0;
}
3. 三种模版
- class template
- Function template
- member template
4. shared_ptr中的member template
template <typename _Tp>
class shared_ptr:public __shared_ptr<_Tp> {
template <typename _Tp1>
explicit shared_ptr(_Tp1* __p) :__shared_ptr(__p){}
};
Base1* ptr = new Derived1; // up-cast
shared_ptr<Base1>stpr(new Derived); // 模拟 up-cast
注意这里的up cast 和 模拟的up cast
5. 关于函数调用
#include <iostream>
using namespace std;
void func(double a) {
std::cout <<"double"<<endl;
}
/*
void func(char a) {
std::cout <<__func__<<" double"<<endl;
} */
/*
void func(int a){
std::cout <<"int"<<endl;
}*/
class base {};
class derived : public base {};
void func1(base * b){
cout<<__func__<<endl;
}
template <class T>
class Ttype{
private:
T* base_;
public:
Ttype(T* base):base_(base) {
cout<<"created"<<endl;
}
};
int main() {
func(int(4)); //ok
derived* pderived;
func1(pderived); // ok
Ttype<base> b(pderived); // ok
return 0;
}
// 打印
func double
func1
created
- 函数调用时,如果有实参-形参完全匹配的则直接调用完全匹配的函数。
- 函数调用时,如果参数找不到完全匹配的类型时,编译器会自动进行实参-形参的转换(up-cast也ok),如果可以转换则调用。
- 函数调用时,如果存在多个实参-形参转换后匹配的函数,则发生ambiguous 。
item10. Specialization/模版特化
模版是泛化。特化是将特定类型的模版特化。
理解 :模版特化的的作用
在已有模版的基础上,对某些类型的重新定义模版函数/类,差别于已有模版的实现。
1. 语法
template <class Key>
struct hash{};
template <>
struct hash<int>{
size_t operator()(int x) const {return x;}
};
struct hash<long>{
size_t operator()(long x) const {return x;}
};
item11. 模版偏特化
偏特化种类
- 个数上的偏
- 范围上的偏
1. 个数上的偏
语法
template <typename T,typename Alloc=...>
class vector{};
template <bool, typename Alloc=....>
class vector{};
注意⚠️:
指定被特话的模版参数的顺序要从左到右,不能跳过左边的参数特化。
2. 范围上的偏
将模版类型改变为模版类型的指针是一种范围上的缩小。
语法
template <class T>
class C{
public:
C(){
std::cout<<"cotr T"<<std::endl;
}
};
template <T>
class C<T*>{
public:
C(){
std::cout<<"cotr T*"<<std::endl;
}
};
C<int>c;
C<int*>d;
item12. 模版模版参数
1. 语法
template <typename T, template <typename T2> class container>
class XCLS{
container<T>c;
};
当模版的参数类型是模版类型时,这就是模版模版参数。
2. 接受两个模版参数的模版模版参数
template <typename T, template <typename T2> class container>
class XCLS{
container<T>c;
};
template <typename T>
using Lst = std::list<T>; // C++11
XCLS<std::string, std::list> mylist1; // error,list 需要指定多个模版参数
XCLS<std::string, Lst> mylist2;
注意⚠️:
容器有多个模版参数,一般使用时只会指定一个模版参数,其他模版参数采用默认值。
3. 接受一个模版参数的模版模版参数
template <typename T, template <typename T2>
class SmartPtr>
class XCLS1
{
SmartPtr<T>sp;
public:
XCLS1():sp(new T){}
};
XCLS1<string,shared_ptr>p1;
item13. C++标准库
动手实践标准库要优于看示例。
item14. 三个主题
1. variadic templates(C++11)
语法
void print(){}
template <typename T, typename ... Types>
void print(const T& firstArg, const Types& ... args){
std::cout<<"size of args : " << sizeof...(args) << std::endl;
std::cout<< firstArg <<std::endl;
print(args...);
};
print(7,"this",4.2);
size of args : 2
7
size of args : 1
this
size of args : 0
4.2
- 一个和一包template 参数。
- ...就是一个所谓的包
- 如何求一包的大小?
sizeof...(args)
- 一个和一包的使用方法很灵活,不必只是递归的使用
2. auto (C++11)
语法
auto是C++11的一个语法糖。
std::list<std::string> c;
auto iter = c.begin();
auto iter_1; // error
iter_1 = c.begin();
注意⚠️:
auto 仅能以“初始化”的方式使用,否则编译器无法推导auto的类型。
3. ranged-base for (C++11)
语法
for (int i :{12,2}){
std::cout<<i<<std::endl;
}
std::list<int> li;
for(auto elem : li) {
std::cout<<elem;
}
for(auto& elem : li) {
std::cout<<elem;
}
item15. Reference
三种类型的变量
- 普通变量
- 指针变量
- 引用变量
- reference本质(编译器实现)是指针,但使用时不能当作指针分析问题。
- reference定义时一定要有初值,指示所代表的变量,不能作为其他变量的引用。
- reference是代表关系,使用reference 就是在使用被reference的变量。
- reference 与其代表的变量,大小和地址都相同,其实是假象。
reference的常见用途
作为参数类型(parameters type)和返回值(return type)类型使用
reference 通常不用于声明变量,而是作为参数类型(parameters type)和返回值(return type)类型使用。
对调用者来说,传引用比传指针一致性更好,更友好。
声明入参为const & 比& 好
如果不想在函数改变入参的值,声明入参为const& 比 &好。原理与const成员函数类似。
class A{};
void func(const A&){};
void func1(A&){}
func(aa); // ok
func1(aa); // error
& 不作为函数签名(signature)的一部分,const 作为函数签名的一部分
double imag(const double& a){};
double imag(const double a){}; // ambiguous;