C++复习
1.绪论
C++和C语言
-
两者之间的联系:C++是C的超集。支持C++语言的编译器必定支持C语言。
-
两者之间的不同:C语言支持面向过程的结构化设计方法。C++语言支持面向对象的设计方法。
-
面向过程的程序设计方法:用于数学计算,缺点是对于庞大、复杂的程序难以开发和维护
-
面向对象的程序设计方法:程序模块的独立性、数据的安全性、程序的可重用性
对象
- 对象由一组属性和一组行为构成。
- 属性:用来描述对象静态特征的数据项。
- 行为:用来描述对象动态特征的操作序列。
- 类与对象的关系:一个属于某类的对象称为该类的一个实例
封装
- 优点:具有隐藏性和安全性,易于维护。缺点:需要更多的输入输出函数。
继承
- 有效地提高了程序的可重用性
- 继承具有传递性
调试程序的方法
- 限定最小出错范围
- 如果依然不能找到错误,则要进入调试状态
2.C++程序设计
变量的存储类型
-
auto 动态临时变量:属于一时性存储,其存储空间可以被若干变量多次覆盖使用。C++新标准中,表示编译器自动推导的类型
-
static(静态变量):在内存中是以固定地址存放的,在整个程序运行期间都有效。
-
register:存放在通用寄存器中。
-
extern:使用另一个文件中定义的变量
数据区
静态存储区
- 全局变量、静态变量、常量
动态存储区
- 栈:局部变量、临时变量
- 堆:动态申请的指针变量
int* array = new int[10];
delete[] array;
int* x = new int;
delete x;
枚举类型—enum
enum Weekday { SUN, MON, TUE, WED, THU, FRI, SAT };
for (int i = 0; i < 7; i++) cout << (Weekday)i << endl;
- 枚举元素具有默认值,它们依次为:0,1,2,……
- 可以在声明时另行指定枚举元素的值:
enum Weekday { SUN = 7, MON = 1, TUE, WED, THU, FRI, SAT };
Weekday w = WED;
cout << w;
int x = SUN;
cout << x;
- 枚举元素是常量,不能对它们赋值,不能写:SUN = 0 ×。将整数值赋给枚举变量时需要进行强制类型转换。
命名空间
- C++标准模板库定义在一个称为 std 的名字空间里。
- 自定义命名空间:
namespace Name {
int x = 2;
}
int main() {
cout << Name::x;
}
输入输出
cout控制输出格式
- 需要包含头文件 #include
precision(n): 设置实数的精度为n位
width(n): 设置字段宽度为n位
fill(c): 设置填充宇符c
cout.width(5);
cout.fill('*');
cout << 5;
简单的I/O格式控制
setprecision(int): 设置浮点数的小数位数(包括小数点)
setw(int): 设置域宽
setprecision(int): 设置浮点数的小数位数(包括小数点)
dec: 数值数据采用十进制表示
hex: 数值数据采用十六进制表示
oct: 数值数据采用八进制表示
int x = 14;
cout << setw(5) << x << endl;
cout << hex << x << endl;
文件输入输出
- 库 fstream 为每种内部类型定义了相应的文件写入和读出方式。
ifstream Input("data.txt");
int x;
Input >> x;
ofstream Output("data__.txt");
Output << x;
Input.close(), Output.close();
3.函数
函数的参数传递机制
- 传递参数值: void fun(int x, int y);
- 传递参数指针(地址): void fun(int* px, int* py);
- 传递参数引用: void fun(int& x, int& y);
引用
- 引用不是值,不占用额外存储内存空间
- 引用在声明时必须初始化
- 引用的初始值可以是一个变量或另一个引用,且类型一致。
(int*)& rp=p; //rp是一个引用,它引用的是指针
(int&)* ra=a; //error
int& ra[10]=a; //error
float f=2;
int& ir3=f; //error
内联函数
- 内联函数编译时,在调用处用函数体进行替换。
- 关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
- 内联函数体内不能有循环语句和switch语句。内联函数不能定义为递归函数。
内联函数与函数宏区别
- 内联函数对函数的参数及返回值有明确定义,增强了代码的安全性。而宏定义没有。
- 内联函数的参数和返回值具有明确的类型标识,宏定义没有。
缺省形参值的函数
- 有缺省参数的形参必须在形参列表的最后
- 调用时实参与形参的结合是从左向右的顺序
- 如果一个函数有原型声明,且原型声明在定义之前,则缺省形参值必须在函数原型声明中给出; 如果声明中给出了缺省形参之,那么定义的时候就不能给出了。
int add(int x = 5, int y = 6);
//原型声明在前
int main(){
add();
}
int add(int x, int y){
//此处不能再指定缺省值
return x + y;
}
重载函数
- 编译器不以返回值来区分
- 编译器不以形参名来区分
- 每个函数的参数表唯一就行(参数个数、参数类型、或参数顺序上有所不同)
- 在重载函数参数存在隐式类型转换情况下,重载函数在调用时存在二义性。这时必须使用显式强制转换完成函数调用。
void print(double a) {
cout << "print_d " << a << endl;
}
void print(long a) {
cout << "print_l " << a << endl;
}
int main(void) {
int a;
print(a); //error
print(double(a)); //ok
print(long(a)); //ok
return 0;
}
4.类和对象
概述
数据成员
- 类声明中的访问限定符private、public、protected没有先后次序之分
- 在一个类中,访问限定符private、public、protected的出现次数没有限制
- 如果没有明确写出访问限定符,则默认成员具有private的访问权限。
- 数据成员的数据类型不能是自身类的对象。此外,数据成员不能指定为自动(auto)、寄存器(register)和外部(extern)存储类型。
C++11标准中关键词auto的新定义
- 声明变量时根据初始化表达式自动推断该变量的类型
- 声明函数时函数返回值的占位符。
成员函数
- 成员函数也可以定义为内联成员函数
- 类成员函数的默认值,一定要写在类声明中,不能写在类声明之外的函数实现中。
关于this指针
- this是成员函数中,指向当前对象自身(即成员函数所属的类对象的首地址)的隐含指针.
- 在类X的非const成员函数里,this的类型就是X *。不能给它赋值,但可以通过它修改数据成员的值。
- 在类x的const成员函数里,this被设置成const X *类型,不能通过它修改对象的数据成员值。
- 静态成员函数没有this指针,因此在静态成员函数中不能访问对象的非静态数据成员。
private与protected的异同
-
在类定义之外, private与protected类型的数据都不能被直接访问
-
在有继承关系的类族中,父类的private数据在子类中不能被直接访问;而父类的protected数据可以在子类中被直接访问,三种继承方式均是如此。
public
- 过多地使用public权限,会破坏数据的封装性、安全性;违背面向对象程序设计的基本原则
- 类的静态属性(数据)设计为private或者protected的;
- 类的动态属性(行为、函数)中一定要有public函数(公有接口),否则外界根本无法使用该类
对象赋值
- 对象名1=对象名2;
- 两个对象必须类型相同
- 进行数据成员的值拷贝,赋值之后,两不相干
- 若对象有指针数据成员,赋值可能产生问题
构造函数和析构函数
构造函数
- 构造函数没有返回类型。
- 构造函数由系统自动调用,不允许在程序中显式调用。
- 构造函数通常应定义为公有成员,尽管是由编译系统进行的隐式调用,但也是在类外进行的成员函数访问。
- 在用默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象所有数据成员初始化为0;如果创建的是局部对象,即不进行对象数据成员的初始化。
- 在类没有定义任何构造函数时,系统才会产生默认构造函数。一旦定义了任何形式的构造函数,系统就不再产生默认构造函数。
拷贝构造函数
-
默认拷贝构造函数以成员按位拷贝(bit-by-bit)的方式实现成员的复制。
-
当一个类有指针类型的数据成员时,默认拷贝构造函数常会产生指针悬挂问题 。
-
对拷贝构造函数的调用常在类的外部进行,应该将它指定为类的公有成员。
Point(const Point& p){
x = p.x;
y = p.y;
}
X obj3 = obj1; //情况1:调用拷贝构造函数
f(obj1); //以对象作函数参数时,调用拷贝构造函数
obj2 = h( ); //以对象作为函数返回值时,调用拷贝构造函数
构造函数与初始化列表
- 构造函数初始化列表中的成员初始化次序与它们在类中的声明次序相同,与初始列表中的次序无关。
Tdate::Tdate(int m,int d,int y):month(m),day(d),year(y){}
Tdate::Tdate(int m,int d,int y):year(y),month(m),day(d){}
Tdate::Tdate(int m,int d,int y):day(d),year(y),month(m){}
- 尽管三个构造函数初始化列表中的month、day和year的次序不同,但它们都是按照month→day→year的次序初始化的
- 构造函数初始化列表先于构造函数体中的语句执行。
- 常量成员,引用成员,类对象成员,派生类构造函数对基类构造函数的调用必须采用初始化列表进行初始化。
析构函数
特点
- 无参数
- 无返回值
- 不能重载:每个类仅有一个析构函数
析构函数调用时机
- 对象生命期结束时自动调用
Point* p = new Point;
delete p;
- 局部对象:定义的语句块结束处
- 全局对象、静态对象:程序结束时
使用析构说明
- 若有多个对象同时结束生存期,C++将按照与调用构造函数相反的次序调用析构函数。
- 每个类都应该有一个析构函数,如果没有显式定义析构函数。C++将产生一个最小化的默认析构函数。
- 构造函数和析构函数都可以是inline函数。
- 在通常情况下,析构函数与构造函数都应该被设置为类的公有成员,虽然它们都只能被系统自动调用的,但这些调用都是在类的外部进行的。
5.类的组合
类组合的构造函数设计
- 先调用内嵌对象的构造函数,然后调用本类的构造函数。
- 析构函数的调用顺序相反
- 组合类构造函数初始化列表中未出现的子对象,用子对象的默认构造函数(即无形参的)初始化
- 系统自动生成的隐含的组合类默认构造函数中,子对象全部用子对象的默认构造函数初始化
前向引用声明
- 尽管使用了前向引用声明,但是在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象。
//不可以: 定义B的对象、调用B的行为
class B; //前向引用声明
class A{
B m_b; //error
void f(B b);{
b.DoSomething(); //error
}
};
class B{
};
- 但是,经过前向引用声明,可以声明类的对象引用
//可以声明: B的形参、引用、指针
class B; //前向引用声明
class A{
void f(B b);
B& rb;
B* pb;
};
class B{
};
6.数据的共享与保护
标志符的作用域与可见性
作用域
- 从小到大:函数原型(指的是声明)、块(比如函数定义)、类、命名空间、文件。
- 函数原型中的参数,具有原型作用域。其作用域始于“(”,结束于“)”.
- 文件作用域,标识符也被称为全局变量。
可见性
-
标志符在其作用域内,并非总是可见的
-
因为作用域嵌套,如果在内层作用域内定义了同名变量,则在内层将不可见外层的同名变量
生存期
静态生存期
- 这种生存期与程序的运行期相同。
- 在函数内部声明静态生存期对象,要冠以关键字static 。
动态生存期
- 开始于程序执行到声明点时,结束于命名该标识符的作用域结束处。
静态成员变量与静态成员函数
静态成员函数
- 没有this指针
- 静态成员函数可以直接调用静态成员变量与静态成员函数,但通过对象才能访问非静态成员函数和非静态成员变量
class A{
static int m_a;
static char s[20];
public:
static void Fun();
};
int A::m_a = 0;
char A::s[20] = "abcdefg";
void A::Fun(){
cout << m_a << endl;
cout << s << endl;
}
int main() {
A::Fun();
}
class A {
static int x;
int y;
public:
A(int yy) :y(yy) {};
void f() {
cout << "f here" << endl;
}
static void g() {
cout << "g here" << endl;
}
static void test(A a) {
g();
a.f();
cout << x << endl;
cout << a.y << endl;
}
};
- 静态成员变量初始化需要在主函数外面进行,前面不可以加static。
- 静态成员函数可以在主函数中调用。
- 如果该类有一个字符串静态成员变量,要这样初始化:
char A::s[20] = "abcdefg";
静态成员变量
- 静态数据成员不是由构造函数创建的,是由变量定义语句创建的:
static int count;
int Point::count = 0;
友元
友元关系的特征
- 单向的
- 不传递的
- 不继承的
共享数据的保护
常类型
- 常类型的对象必须进行初始化,而且不能被更新。
- 常对象:必须进行初始化,不能被更新。
- 常引用:被引用的对象不能被更新。注意,常量的引用必须是常对象。
- 常数组:数组元素不能被更新
- 常指针:指向常量的指针
常成员
- 常对象只能调用常成员函数。
- 普通对象调用普通函数;普通对象也能调用常函数。
- const可以用于函数重载,const函数传递的是const this指针。
//一个重载的例子
void print();
void print() const; //常对象只能调用这个函数
常函数
- 常成员函数只能调用常成员函数,不能调用普通函数。
- 普通函数既可调用常成员函数,也可调用普通函数。
常引用作形参 const &
- 提升函数调用时传参的效率——传引用&
- 不允许函数改变实参——保护实参
编译命令
编译预处理命令
- #include 包含指令
- #define 宏定义指令
- #undef
条件编译指令
#if 常量表达式1
程序正文1 //当“ 常量表达式1”非零时编译
#elif 常量表达式2
程序正文2 //当“ 常量表达式2”非零时编译
#else
程序文3 //其他情况下编译
#endif
7.继承和派生
派生类的生成
过程
- 吸收基类成员
- 改造基类成员
- 添加新的成员
派生类不能继承基类以下内容
- 基类的构造函数和析构函数
- 基类的友元函数(友元不具有继承性)。
- 静态数据成员和静态成员函数
继承类型
三种继承方式
- 公有继承
- 私有继承
- 保护继承
注:class的默认继承方式是私有继承
基类中protected的成员
• 类内部:可以访问
• 类的使用者:不能访问
• 类的派生类成员:可以访问
公有继承(public)
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
- 通过派生类的对象只能访问基类的public成员。
私有继承(private)
-
基类的public和protected成员都以private身份出现在派生类中
-
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
-
通过派生类的对象不能直接访问基类中的任何成员。
保护继承(protected)
- 基类的public和protected成员都以protected身份出现在派生类中
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
- 通过派生类的对象不能直接访问基类中的任何成员
类型兼容规则
一句话概括,就是只能用派生类初始化基类,反之则不行。
- 派生类的对象可以赋值给基类对象。
Derived d;
Base b = d;
- 把派生类对象的地址赋值给基类指针
Derived d;
Base *b = &d;
- 用派生类对象初始化基类对象的引用
Derived d;
Base &b = d;
类型兼容规则说明
- 类型兼容规则只针对于公有继承,在私有继承和保护继承中,不能将派生类对象赋值给基类对象。
多继承
多继承时派生类的声明
class 派生类名:继承方式1 基类名1,继承方式2 基类名2, ...
{
成员声明;
};
class C : public A, private B
{
public:
void setC(int, int, int);
void showC() const;
private:
int c;
};
派生类的构造和析构函数
继承时的构造函数
- 基类的构造函数不被继承,派生类中需要声明自己的构造函数。
- 定义构造函数时,需要对本类中新增成员进行初始化,对继承来的基类成员的初始化,自动调用基类构造函数完成。使用“初始化列表”自动调用基类构造函数。
派生类构造函数的执行顺序
- 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
- 对新增成员变量(内嵌对象)进行初始化,初始化顺序按照它们在类中声明的顺序。
- 执行派生类的构造函数体中的内容。
拷贝构造函数
-
若建立派生类对象时没有编写拷贝构造函数,编译器会生成一个隐含的拷贝构造函数,该函数先调用基类的拷贝构造函数,再为派生类新增的成员对象执行拷贝。
-
若编写派生类的拷贝构造函数,则需要为基类相应的拷贝构造函数传递参数。例如:
C::C(const C &c1): B(c1) {…}
继承时的析构函数
- 析构函数也不被继承,派生类自行声明
- 析构函数的调用次序与构造函数相反。
派生类成员的标识与访问
同名隐藏规则
当派生类与基类中有相同成员时:
- 若未强行指明,则通过派生类对象使用的是派生类中的同名成员。
d.Show(); = d.Derived::Show();
- 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名限定。
d.Base::Show();
二义性
- 问题一:基类间同名
class A {
public:
void f();
};
class B {
public:
void f();
void g();
};
class C: public A, public B
{
public:
void g();
void h();
};
//如果定义:C c1; 则 c1.f() 具有二义性, 而 c1.g() 无二义性(同名隐藏)
- 问题二:多级派生中有共同基类
class B {
public:
int b;
};
class B1 : public B{
//...
};
class B2 : public B{
//...
};
class C : public B1, public B2{
//...
};
/*有二义性:
C c;
c.b
c.B::b
无二义性:
c.B1::b
c.B2::b
*/
虚基类
- 为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝
- 在第一级继承时就要将共同基类设计为虚基类
class B { public: int b; };
class B1 : virtual public B { public: int b1; };
class B2 : virtual public B { public: int b2; };
class C : public B1, public B2 { public: float d; };
- 建立对象时所指定的类称为最(远)派生类。
- 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
- 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略。
8.多态
概念
多态性的类型
- 重载多态:函数重载、运算符重载
- 强制多态:隐式类型转换
- 参数多态:模板
- 包含多态:类族中的不同类中同名成员函数的多态行为
联编(绑定binding)
把一个标志符和一个存储地址联系在一起的过程
-
静态联编(绑定)(编译时多态性):绑定过程完成在编译阶段
-
动态联编(绑定)(运行时多态性):绑定过程完成在运行阶段
静态联编(绑定)
- 重载多态(函数和运算符的重载)
- 强制多态(隐式类型转换)
- 参数多态(模板)
动态联编(绑定)
- 包含多态(继承和虚函数)
虚函数
C++实现运行时多态性的关键途径:类型兼容+虚函数
虚函数的定义前提
- 虚函数重定义时,函数原型要完全一致,包括:返回类型,函数名、参数个数、参数类型、顺序必须与原函数完全一致。
虚函数的定义
- virtual可以只修饰基类的函数,而子类中的同型函数前可以省略virtual关键字。这样的函数族依然是虚函数
虚函数类族的构造函数、析构函数
- 没有虚构造函数,没有虚拷贝构造函数
- 有虚析构函数:有virtual函数类族的析构函数,一定是虚析构函数
虚函数定义格式
- 在基类中,申明虚函数的语法:virtual <类型> 函数名(参数表);
- 类外定义虚函数时,可以不写关键字virtual了,虚函数的形参默认值要写在基类中
class Pet{
public:
virtual void speak(){
cout << "How does a pet speak ?" << endl;
}
virtual ~Pet(){}
};
class Cat : public Pet{
public:
void speak(){
cout << "miao!miao!" << endl;
}
~Cat(){}
};
class Dog : public Pet{
public:
void speak(){
cout << "Wang!Wang! " << endl;
}
~Dog(){}
};
int main()
{
Pet* p1;
Cat Huahua;
p1 = &Huahua;
p1->speak();
return 0;
}
//Output: miao!miao!
//这就是多态,让基类对象的指针指向派生类,可以调用派生类的函数speak()。
//如果不用virtual,那么 p1->speak() 调用的将是基类中的speak().
//如果是Pet obj = Huahua,那么调用的仍是基类的函数,因为调用子类虚函数只能通过指针或者引用,不能通过对象。
虚函数的使用限制
- 通过基类的指针、引用调用子类虚函数;
- 通过基类对象名不能调用到子类虚函数
- 通过变量的引用调用虚函数有缺陷:引用一旦初始化后,就无法重新赋值
Pet& a = Huahua;
a.speak();
a = Benben;
a.speak();
int x = 2, y = 3;
int& z = x;
cout << z << endl;
z = y;
cout << z << endl;
/*
Output:
miao!miao!
miao!miao!
2
3
*/
虚析构函数
- 不能定义虚构造函数。
- 可以定义虚析构函数:若某类中定义有虚函数,则其析构函数也应当说明为虚函数。特别是在析构函数需要完成一些有意义的操作——比如释放内存时,尤其应当如此。
纯虚函数
class 类名
{…
virtual 返回值类型 函数名(参数表) = 0;
…
};
- 纯虚函数在基类中申明后,不能在基类中定义函数体。纯虚函数的具体实现只能在派生类中完成
抽象类
- 如果一个类中至少有一个纯虚函数,那么这个类成为抽象类(abstract class)
- 不能申明抽象类对象
- 只作为基类被继承,无派生类的抽象类毫无意义
- 可以定义指向抽象类的指针和引用,它们必然指向或引用派生类对象
运算符重载
运算符函数与运算符重载
-
2种重载形式:成员函数 或 友元函数
-
C++把重载的运算符视为特殊的函数,称为运算符函数。
-
运算符重载是函数重载的一种特殊情况
流运算符重载
重载方式:非成员函数(友元函数)
- 类中声明
friend ostream& operator<<(ostream& o,const Clock& c); // 这个地方最好要写成const引用!
friend istream& operator>>(istream& i, Clock& c); //看清,这个是要Clock的引用
- 可以在类外定义
ostream& operator<<(ostream& o, const Clock& c){
return o << c.Hour << ":" << c.Minute << ":" << c.Second;
}
- 相当于调用函数
operator<<(cout, c3);
运算符重载的规则
- “运算符重载”是针对C++中原有运算符进行的,不可能通过重载创造出新的运算符。
- 除5个运算符外: . . :: ?: sizeof 其他运算符都可以重载。*
- 不得为重载的运算符函数设置默认值,调用时也就不得省略实参。
- 除了new和delete这两个较为特殊运算符以外,任何运算符如果作为成员函数重载时,不得重载为静态函数。
- =、[]、()、->以及所有的类型转换运算符只能作为成员函数重载,且不能是针对枚举类型操作数的重载。
- 重载的运算符保持其原有的操作数个数、优先级和结合性不变。
- 语义要与已知功能保持一致。比如:不能把“+”重载成“-”
重载为成员函数
- 对象自身是一个操作数,形参表中的参数个数要比运算目数少1
单目前置运算符的重载
- 由于要改变第一操作数,所以传递引用。返回的是引用。
- 单目前置自增运算符:
CMoney& operator++(); //重载为成员函数
friend CMoney& operator++ (CMoney& c);
单目后置运算符的重载
-
由于要改变第一操作数,所以传递引用。注意返回的是值。
-
单目后置自增运算符
CMoney operator++(int i); //重载为成员函数
//或 CMoney operator++(int);
friend CMoney operator++ (CMoney& c, int i); //重载为友元函数
重载类型转换符
- 类型转换符必须作为成员函数重载
- 在重载类型转换符时,由于运算符本身已经表示出返回值类型,因此不需要返回值类型的声明
operator long() const{
return num/den;
}
重载赋值运算符“=”
-
f1 = f2;
-
赋值运算符只能作为成员函数重载。
-
返回值应声明为引用,而函数体中总是用语句
return *this;
返回。
fraction& operator=(fraction f){
num=f.num;
den=f.den;
return * this;
}
-
注意,对于任何类,即使没有重载赋值运算符,仍然可以使用运算符=。在这种情况下,默认的赋值操作就是同类对象之间对应成员的逐一赋值。
-
需要重载赋值运算符的情况:类中包含指向动态空间的指针。
-
一个类如果需要重载运算符=,通常也就需要定义自己特有的拷贝构造函数,反之亦然。
-
如果参数被声明为指向同类对象的引用或指针,应判别所指向对象的是否与被赋值对象为同一对象,如果是,立即返回,不做任何赋值处理。
重载复合赋值运算符"+="
fraction& operator+= (fraction& f1,fraction f2){
f1=f1+f2;
return f1;
}
重载下标访问运算符"[ ]"
int & operator[ ](int i);
有左值操作数
- 左值就是数据的地址,右值就是数据本身。每个数据都有右值,但只有部分数据有左值;典型的有左值数据就是变量,典型的无左值数据就是常量。
- 增1减1运算符(++、–)、赋值运算符(=)、复合赋值运算符(+=、*=等)以及取地址运算符(&)都要求其第一操作数必须是有左值的操作数。
- 要做到第一操作数为有左值的操作数,在这些运算符作为非成员函数重载(=只能作为成员函数重载)时,对应于第一操作数的第一参数必须声明为引用参数。在作为成员函数重载时,第一操作数就是该对象本身,因此函数体中须用
return \*this;
返回。 - 增1减1运算符(++、–)、赋值运算符(=)、复合赋值运算符(+=、*=等)都要修改第一操作数。
引用参数的优点:
- 当对象很大或需要深层复制时,可大大减少对资源的占用,提高参数传递的效率,但无法利用系统的自动转换机制。
作为成员函数重载还是作为非成员函数重载?
- =、[]、()、->以及所有的类型转换运算符只能作为成员函数重载。
- 如果允许第一操作数不是同类对象,而是其他数据类型,则只能作为非成员函数重载(如输入输出流运算符 >> 和 << )。
9.模板
- 模板是泛型技术的基础
- 参数化多态
函数模板
函数模板格式
template <typename T>
类型名 函数名(参数表)
{函数体的定义}
template <class T>
类型名 函数名(参数表)
{函数体的定义}
模板形参表 <typename T>
- <模板形参表>中的参数必须是惟一的,而且在<函数定义体>中至少出现一次。
- 函数模板定义不是一个实实在在的函数,编译系统不为其产生任何执行代码。
- 函数模板只是说明,不能直接执行,需要实例化为模板函数后才能执行。
模板函数
- 函数模板只是说明,不能直接执行,需要实例化为模板函数后才能执行
- 模板不支持隐式的类型转换
- 对模板函数的说明和定义必须是全局作用域
类模板
类模板格式
template <模板参数表>
class 类名
{ 类成员声明 };
类模板外定义成员函数
template <模板参数表>
类型名 类名<T>::函数名(参数表)
template<typename T>
class A {
T a;
public:
A(T a_);
T getA();
};
template<typename T>
A<T>::A(T a_) :a(a_) {}
template<typename T>
T A<T>::getA() {
return a;
}
int main() {
A<long long> atmp(1);
cout << atmp.getA() << endl;
}
- 模板要写在h文件中
- 模板的实现部分要写在h文件中
函数模板的特化
template <>
返回类型 函数名<特化的数据类型>(参数表) {}
template<> //(b)模板函数f(T)特化,即(a)的特化
void f(int*){ cout << "specialization template f(T)" << endl;}
当程序中具有普通函数、函数模板及其特化时,其匹配过程如下:
- 若有普通函数匹配成功,则选中
- 否则,查找函数模板,若找匹配模板(多态时查找最匹配模板)则
- 若在匹配模板中找到对应的特化,则选择特化;
- 否则(无特化),按匹配模板生成模板函数
- 否则,匹配失败
类模板的实例化
类模板名<模板参数名表> 类对象名称;
- 定义指针时不会引起实例化
- 在不定义任何对象的时候,可进行显式实例化声明:
template class A<int>;
类模板的特化
- 特化整个类模板
- 偏特化:特化类模板的一部分参数或成员函数
- 特化整个类模板的方法
template < >
class 类模板名<特化数据类型> {
……
}
-
类模板的偏特化
- 特化类模板的部分类型
template <class T2> class 类模板名<特化部分数据类型T1, T2>{ ……//类成员定义 }
- 特化类模板的部分成员函数
template <> 返回类型 类模板名<特化的数据类型>::特化成员函数名(参数表){ …… //函数定义体 }
类模板与友元
- 普通友元
template<class T>
class A{
class c1;
friend void func1();
//...
};
- 模板友元
template<class type>
class A{
template<class T> friend class c2;
template<class T> friend void fun(T u);
//...
};
- 模板特例友元:友元与类模板的类型参数相同
template <class T> class c3; //申明类模板
template <class T> void fun3(T u); //声明函数模板
template<class T>
class A{
friend class c3<T>; //类模板的特例
friend void fun3<T>(T u);
//...
};
元编程(metaprogramming):
-
编写可以一类特殊的程序,这类程序可以修改别的程序或者其自身。
-
c++通过模板template实现元编程
-
子类型多态 (subtype polymorphism)
- 通过虚函数和继承实现多态
- 面向实例 ( instance, 即 object ) 编程
- 虚函数采用动态绑定,运行时速度较慢
-
模板多态
- 函数模板和类模板
- 面向模板编程
- 模板是静态绑定(由编译完成),运行时较快
10.STL
- 提供了模板化的通用类和通用函数
- STL的核心内容包括容器、迭代器、算法三部分
- 程序 = 数据结构 + 算法
容器
- 用来存储其他对象的对象,它是用模板技术实现的。
- STL的容器常被分为顺序容器、关联容器和容器适配器三类。
- 顺序类型容器:向量(vector)、链表(list)、双端队列(deque)。
- 关联容器:集合(set)、多重集合(multiset)、map/multimap
- 容器适配器:堆栈(stack)、队列(queue) 、优先队列(priority_queue)
关联式容器
-
set和multiset会根据特定的排序准则,自动将元素排序,两者提供的操作方法基本相同,只是multiset允许元素重复而set不允许重复。
-
map中的元素不允许重复,而multimap中的元素是可以重复的。
-
特点:
-
键查找
-
对元素快速访问
-
允许插入,但不能指定位置
-
用树实现
-
-
无序关联容器,基于哈希表,unordered_set,unordered_multiset,unordered_map,unordered_multimap
-
set和map在STL中的组织形式为高效的平衡二叉树结构
-
set和map的插入删除比其他容器效率高。插入删除时仅修改指针指向即可,没有内存的移动。
-
set和map的插入删除后元素的 iterator 不变。
迭代器
-
迭代器(iterator)是一个对象,常用它来遍历容器,即在容器中实现“取得下一个元素”的操作
-
迭代器提供的基本操作包括:
- 在容器中的特定位置定位迭代器。
- 在迭代器指示位置检查是否存在对象。
- 获取存储在迭代器指示位置的对象值。
- 改变迭代器指示位置的对象值。
- 在迭代器指示位置插入新对象。
- 将迭代器移到容器中的下一个位置。
迭代器分类
-
输入迭代器
- 可以用来从序列中读取数据
- =, *, ++
-
输出迭代器
- 允许向序列中写入数据
- =, *, ++ , ==, !=, ->
-
前向迭代器
- 可以对序列进行单向的遍历
- =, *, ++ ==, !=, ->
-
双向迭代器
- 可在两个方向上对数据遍历
- =, *, ++, ==, !=, ->, –
-
随机访问迭代器
- 也是双向迭代器,可在序列中的任意两个位置间跳转
- =, *, ++, ==, !=, ->, – , +=, -=, ->, +, -, [n], <, <=, >, >=
算法
-
算法常常通过迭代器间接地操作容器元素,而且通常会返回迭代器作为算法运算的结果。
-
STL大约提供了70个算法,每个算法都是一个模板函数或者一组模板函数
-
大致分为这四类:
- 不可变序列算法(Non-mutating algorithms)
- find, count, search, sort
- 可变序列算法(Non-mutating algorithms)
- 排序和搜索算法
- 数值算法
- 不可变序列算法(Non-mutating algorithms)
函数对象
- 任何普通函数和任何重载了运算符operator()的类的对象都满足函数对象的特征。
、双端队列(deque)。 - 关联容器:集合(set)、多重集合(multiset)、map/multimap
- 容器适配器:堆栈(stack)、队列(queue) 、优先队列(priority_queue)
关联式容器
-
set和multiset会根据特定的排序准则,自动将元素排序,两者提供的操作方法基本相同,只是multiset允许元素重复而set不允许重复。
-
map中的元素不允许重复,而multimap中的元素是可以重复的。
-
特点:
-
键查找
-
对元素快速访问
-
允许插入,但不能指定位置
-
用树实现
-
-
无序关联容器,基于哈希表,unordered_set,unordered_multiset,unordered_map,unordered_multimap
-
set和map在STL中的组织形式为高效的平衡二叉树结构
-
set和map的插入删除比其他容器效率高。插入删除时仅修改指针指向即可,没有内存的移动。
-
set和map的插入删除后元素的 iterator 不变。
迭代器
-
迭代器(iterator)是一个对象,常用它来遍历容器,即在容器中实现“取得下一个元素”的操作
-
迭代器提供的基本操作包括:
- 在容器中的特定位置定位迭代器。
- 在迭代器指示位置检查是否存在对象。
- 获取存储在迭代器指示位置的对象值。
- 改变迭代器指示位置的对象值。
- 在迭代器指示位置插入新对象。
- 将迭代器移到容器中的下一个位置。
迭代器分类
-
输入迭代器
- 可以用来从序列中读取数据
- =, *, ++
-
输出迭代器
- 允许向序列中写入数据
- =, *, ++ , ==, !=, ->
-
前向迭代器
- 可以对序列进行单向的遍历
- =, *, ++ ==, !=, ->
-
双向迭代器
- 可在两个方向上对数据遍历
- =, *, ++, ==, !=, ->, –
-
随机访问迭代器
- 也是双向迭代器,可在序列中的任意两个位置间跳转
- =, *, ++, ==, !=, ->, – , +=, -=, ->, +, -, [n], <, <=, >, >=
算法
-
算法常常通过迭代器间接地操作容器元素,而且通常会返回迭代器作为算法运算的结果。
-
STL大约提供了70个算法,每个算法都是一个模板函数或者一组模板函数
-
大致分为这四类:
- 不可变序列算法(Non-mutating algorithms)
- find, count, search, sort
- 可变序列算法(Non-mutating algorithms)
- 排序和搜索算法
- 数值算法
- 不可变序列算法(Non-mutating algorithms)
函数对象
- 任何普通函数和任何重载了运算符operator()的类的对象都满足函数对象的特征。
- 函数指针作为函数参数——回调函数