系列文章目录
其他补充
运算符重载
// 用于 if(obj) 中的obj的判断
operator void*()
{
return (void*)0; // or return (void*)1;
}
类型转换重载
operator typeName(); 虽然该函数没有声明返回类型,但应返回所需类型的值。使用转换函数时要小心,可以在声明构造函数时使用关键字explicit,以防止它被用于隐式转换。
- 转换函数必须是类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
函数默认值要在调用之前
变量名是内存的别名,引用是变量的别名;引用之于被引用对象犹如变量名之于内存。
// 严重错误,const引用临时变量,函数结束时该临时变量将不存在
const string& fun()
{
string tmp;
return tmp;
}
模板
与模板相关的关键字
template, class, typename, decltype, auto
函数模板
函数,模板具体化,模板都可以重载
模板实例化:
具体化:
void add(int a, int a){}
template<typename T>
void add(T a, T b){}
template<>
void add(int a, int b){}
template<>
void add<int>(int a, int b){}
add(1.1, 2.1);
add<float>(1.1, 2.1);
template<typename T>
void tfn(T* a) {}
当a为 int*时,可自动推断 T = int
decltype
decltype(expression) var;
- 如果expression是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符。
- 如果expression是一个函数调用则var的类型与函数的返回类型相同。
- 如果expression是一个左值,则var为指向其类型的引用。一步步来,第一步都不通过才进入第三步
- 如果前面的条件都不满足,则var的类型与expression的类型相同。
trailing return type
template<class T1, class T2>
auto gt(T1 x, T2 y) -> decltype(x + y)
{
return x + y;
}
类模板
template, class, typename, decltype, auto
模板是C++编译器指令
模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此, 最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
template<typename T>
class Base {
public:
void fun();
template<typename T2> void fun(T2 a);
};
template<typename T>
void Base<T>::fun() {}
template<typename T>
template<typename T2>
void fun(T2 a) {}
模板支持常规类的技术
模板也支持递归:vector<vector<int>>
默认类型模板参数
template<typename T1, typename T2 = int> class Topo {...};
int 用在模板中
int指出n的类型为int。这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式(expression)参数。
template<class T, int n> class ArrayTP
{
private:
T ar[n];
public:
ArrayTP() {};
explicit ArrayTP(const T& v);
virtual T& operator[](int i);
virtual const T& operator[](int i) const ;
};
模板只有当被使用时才会实例化
具体化
template<class T1, class T2>
class Base{...};
// 部分具体化
template<class T1> Base<T1, int> {...};
//
template<> Base<int , int> {...};
template<class T1> Base<T1*, T1&> {...};
template<class T1, class T2> Base<T1*, T2*> {...};
nested
template<class T> class Beta
{
private:
template<typename V> class hold
{
private:
V va;
public:
hold(V v = 0) : val(v) {}
...
};
hold<T> q;
hold<int> n;
public:
Beta(T t, int i): q(t), n(i) {}
};
将模板用作参数
模板可以包含类型参数(如 typename T)和非类型参数(如 int n)。模板还可以包含本身就是模板的参数。
template<template <typename T> class Thing> class Crab {} ;
模板参数是template <typename T> class Thing
,其中template <typename T> class
是类型Thing是参数。
如:Crab<King> legs;意味着King是模板类,即template<typename T> class King {…};
template<template <typename T> class Thing>
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
public:
Crab() {};
// assume the thing class has push() and pop() members
bool push(int a, double x) {return s1.push(a) && s2.push(x);}
bool pop(int &a, double &x) {return s1.pop(a) && s2.pop(x);}
};
// use
Crab<Stack> nebula; // Stack must match template<typename T> class thing {...};
template<template<typename T>class Thing, typename U, typename V, int n>
class Crab {Thing<U> s1; Thing<V> s2; };
模板与friend
// 约束模板友元
template<typename T> void counts();
template<typename T> void report(T&);
template<typename TT>
class HasFriendT
{
friend void counts<TT>(); // 函数模板的具体化
friend void report<>(HasFriendT<TT>&); // 函数模板的具体化
};
// 非约束模板友元函数
template<typename T>
class ManyFriend
{
template<class C, class D> friend void show2(c&, D&);
};
可变参数模板
略
多文件程序
head.h
#ifndef HEAD_H_
#define HEAD_H_
int fun(int a, int b)
{ return a+b; }
#endif
test1.cpp
#include <cmath> // 从默认系统库中搜索
#include "head.h" // 从当前目录下搜索头文件
void fun2(int, int);
int main(int argc, const char** argv)
{
int a = 0, b = 1;
fun(a, b);
fun2(a, b);
}
test2.cpp
#include "head.h"
int fun2(int a, int b)
{ fun(1, 2); return a+b; }
以上在连接时将报错:
multiple definiton of ‘func(int, int)’
在两个.cpp文件中同时存在fun函数的定义
解决方法
- 在head.h中将函数定义改为声明,定义挪入.cpp中
- 将head.h中的函数定义改为模板
- 将head.h中的函数改为inline
注:inline是对编译器的一种请求,inline不是函数;模板特例化是函数定义;
内存管理
- 自动存储持续性:栈,寄存器
- 静态存储持续性:全局区、全局静态区、局部静态区
存储在全局区,整个程序运行期间都存在。默认初始化0。c++变量遵循单定义规则。int a;
全局变量,可以被其它文件引用static int a;
只能在本文件内引用{static int a;}
局部静态变量,只能在局部引用
#include <cmath>
int x; // 0初始化
int y = 5; // 常量表达式初始化
long z = 13 * 13; // 常量表达式初始化
const double pi = 4.0 * atan(1.0); // 动态初始化
- 线程存储持续性:略
- 动态存储持续性:堆
函数
C++函数的作用域可以是整个类或整个命名空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只能对它自己是可见的,因此不能被其他函数调用。这样的函数将无法运行)。
外部链接性的函数:
extern return_type fun_name(parameter list); // extern是可选项
内链接性:
static int private_f(double x);
static int private_f(double x)
{
...
}
如果在程序的某个文件中调用一个函数。如果该函数是静态的,则在该文件中找;如果不是静态的则在全文件中找,若找到连个两个定义则报错;如果都没找到,则在库中搜索。如果定义了与库函数同名的函数,则会使用用户定义的函数。
变量
变量是如何创建的(由编译器自动创建还是手动new),变量数据的存储位置,变量的作用域,链接性
regitster
关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量
register int count_fast; // request for a register variable
这为了提高变量的访问速度。
如今在C++中此关键字几乎无用,保留主要是为了兼容旧代码。
volatile
告诉编译器不要对变量进行优化。
mutable
即使类变量为const,某个成员也可以被修改。
struct data{
char name[30];
mutable int accesses;
...
};
const data veep = {"abc", 3};
veep.accesses = 30; // allowed
const
在C++中,const限定符对全局变量的默认作用范围进行了修改。
const int cina = 10;
实际上是static的,即只能在本文件中使用,可 以通过包含const头文件的方式使用,因为是const的所以cosnt是否是外部或内部不影响。
可以通过extern const int states = 50;
声明外部链接
extern “C”
g++编译器将以C的语法编译此代码,即fun函数不会编译成fun_i_c_而是_fun
面向对象编程oop
通常我们将类的声明放在.h文件中去,而将实现放到.cpp中去。
调用默认构造函数时不要加(),因为像函数调用。
类中定义的成员函数被所有对象所共用,通过this指针确定对象。
static成员函数没有this指针。
构造与析构
int main(int argc, const char** argv)
{
Clas() = default; // 纯种默认构造函数,显示的让编译器生成,否则编译器不生成
Clas str("default construct"); // 默认构造
Clas str2; // 默认构造
Clas str3 = Clas("default construct"); // 默认构造(可能会创建临时对象,也可能不会,我这里不会)
str2 = str3; // operator=
str2 = Clas("tmp then operator="); // 先用默认构造出一个临时对象,再operator=
// 临时对象使用完后立即析构,有些编译器也可能过会在析构
// 构造顺序 str str2 str3,析构顺序与之相反
}
如果对象在初始化时,是通过其他对象初始化的则调用拷贝构造函数,否则调用默认构造函数
ClassType obj(…); ClassType obj = ClassType(…); // 默认构造
ClassType obj(otherobj); 等价于 ClassType obj = otherobj; // 拷贝构造
const
const Cls str1 = {}; // 列表初始化
...
const Cls& show(const Cls& sr) const;
// 括号中的const表明不修改sr对象,括号外的const表明不修改*this
// 类似const Cls& show(const Cls* this, const Cls& sr);
// 类似 str.show() <=> show(&str)
const CLASS a = {};
a.show(); // show必须是 return_type show()const;
类
声明类指是描述了对象的形式,并没有创建对象,在创建对象前,将没有用于存储值的空间。
class Bakery
{
private:
const int Months = 12; // 行不通
double costs[Months];
...
};
// 两种方法解决
// 1.
// 用这种方式声明枚举并不会创建类数据成员,所有对象中都不包含枚举。Months只是
// 一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用30来替换它
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
...
};
// 2.
// 创建static常量,存储在静态区,可被所有Bakery对象共享
class Bakery
{
private:
static const int Months = 12;
double costs[Months];
...
};
类内枚举
// 传统枚举如果枚举量的名称相同将发生冲突
// 类内枚举
enum egg_old {SMALL, MEDIUM, LARGE}; // unscoped
enum class t_shirt {SMALL, MEDIUM, LARGE}; // scoped
egg_old one = SMALL; // unscoped
t_shirt rolf = t_shirt::MEDIUM; // scoped
int king = one; // implicit type conversion for unscoped
int ring = rolf; // × not allowed, no implicit type conversion
if (king < LARGE) // allowed
std::cout << "LARGE converted to int before comparison.\n";
if (king < t_shirt::MEDIUM) // × not allowed
std::cout << "Not allowed:< not defined for scoped enum.\n";
// 显示类型转换
int Frodo = int(t_shirt::SMALL); // Frodo set to 0
// 显示指定枚举类的底层依赖
enum class : short pizza {SMALL, MEDIUM, LARGE};
static
int ClassName::static_number = 0;
这条语句将静态成员static_number的值初始化为0 。不能在类声明中初始化静态成员变量,这是因为声明表述了如何分配内存,但并不分配内存。可以使用这种格式ClassName obj;来创建对象,从而分配和初始化内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static 。
初始化是在方法文件中,而不是在类声明文件中进行的,因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。
delete 定位new
delete空指针是合法的,没有副作用。
delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。delete/[]知道从new/[]中分配了多少内存。
// 先通过new申请块内存
char* pch = new char[512];
// 通过定位new分配
Clas* pc1 = new(pch)Clas();
// 再分配
Clas* pc2 = new(pch+sizeof(Clas)) Clas();
// 显示调用析构函数
pc2->~Clas();
pc1->~Clas();
// 释放通过new[]分配的内存
delete[] pch;
嵌套结构和类
绝大部分情况下,private静态/非静态成员数据/函数只能内部访问
构造
构造函数():初始化列表
{ 函数体,对象在执行这里的代码之前被创建 }
只有构造函数可以使用这种初始化列表语法
例如,对于const类成员,必须使用这种语法。对于被声明为引用的类成员,也必须使用这种语法。
TODO:拷贝构造做了什么
编译器默认生成的:
默认、拷贝、赋值、析构
如果将一个类对象复制或赋值给另一个类对象,逐成员赋值将使用成员类定义的复制构造函数和赋值运算符。
类内初始化
类继承
public继承建立 is-a 关系
水果是基类,香蕉是派生类
编译器生成的代码将在程序执行时,根据对象类型将虚函数名关联到 基类::fun或 派生类::fun中,总之编译器对虚方法使用动态联编。
空类对象的大小为1字节,含有虚函数的空类大小为虚表指针大小(32 bits/ 64 bits)
子类方法会将同名的父类方法隐藏
class Base
{
private:
/* data */
public:
virtual void base_fun1()
{
std::cout << "base fun1" << std::endl;
}
};
class Derive:public Base
{
private:
/* data */
public:
;
void base_fun1(int a){
;
}
};
int main()
{
map<int, int> mp = {{1,2},{3,5}};
Derive d;
d.Base::base_fun1();
}
抽象基类与protected
class BaseEllipse
{
protected:
double x_;
double y_;
public:
virtual void move(double nx, double ny) = 0;
virtual double area() const = 0;
BaseEllipse(double nx = 0, double ny=0):x_(nx), y_(ny) {}
virtual ~BaseEllipse() {}
};
class Ellipse : public BaseEllipse
{
protected:
double a_;
double b_;
double angle_;
public:
Ellipse(double nx=0, double ny=0, double a=0, double b=0, double angle=0):
BaseEllipse(nx,ny), a_(a), b_(b), angle_(angle) {}
virtual ~Ellipse() {}
virtual void move(double nx, double ny)
{
x_ = nx;
y_ = ny;
}
virtual double area() const
{
return 3.14159 * a_ * b_;
}
void rotate(double nang)
{
angle_ += nang;
}
void scale(double sx, double sy)
{
a_ *= sx;
b_ *= sy;
}
};
class Circle : public BaseEllipse
{
protected:
double r_;
public:
Circle(double x=0, double y=0, double r=1):
BaseEllipse(x, y), r_(r) {}
virtual ~Circle() {}
virtual void move(double nx, double ny)
{
x_ = nx;
y_ = ny;
}
virtual double area() const
{
return 3.14159 * r_ * r_;
}
void scale(double ss)
{
r_ *= ss;
}
};
void mpf(BaseEllipse* p)
{
std::cout << p->area();
}
int main()
{
Circle c;
mpf(&c);
}
子父类的构造、析构、拷贝构造与赋值运算符
- 子类没有new对象
在创建子类时,会自动调用父类组件的四类函数。构造时自动调用,析构时同样如是。所以只需要管理好子类中的对象即可,继承的组件会自动调用对应的默认函数 - 子类有new对象
析构: 析构函数会自动调用基类析构;(析构函数不重载)
构造: 若子类构造函数(包括重载形式)没有在初始化列表中显示写出父类的构造,则自动通过父类的默认构造函数(非重载形式)初始化父类部分的组件。
拷贝构造: 同构造,建议在初始化列表中显示调用父类的copy constructor
赋值运算符: 显示调用父类的 operator=,如果不显示调用,则父类组件没有赋值
如果子类中没有new的成员,则不需要特别定义四类函数。
explicit
class Student
{
private:
int a_;
public:
explicit Student(int a):a_(a) {}
int GetVal()
{
return a_;
}
};
int main()
{
Student b(4);
b = 5; // 构造函数添加了 explicit 禁止隐式类型转换,所以报错
cout << b.GetVal() << endl;
}
private 继承 和 成员对象有可比较之处
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
protected 继承是对private继承的扩展,主要区别体现在三代继承中。
虚基类的构造不具传递性。如果不显示指定则默认调用虚基类的默认构造函数。
在多继承的情况下可考虑,将每个类的函数只处理自己的变量而将基类组件交由显示调用基类函数。
注意
当为非虚基类时(Base*)(Dev1*)&obj和(Base*)(Dev2*)&obj
的地址不同,当为虚基类时地址相同,但其和(Dev2*)&obj
的地址也不同。