目录
内容有点多,主要是根据一学期以来学校的课件进行总结,加入了个人的一些解释。
1.预备知识
1.1类型转换
1.1.1 基本类型
short,char,int ,float,double,unsigned,signed之间均可以发生相互转换。
1.1.2 字符串类型
const char*,string之间
const char*到string可以发生隐式转换
string到const char*之间不能发生转换。
例如
#include<bits/stdc++.h>
using namespace std;
class Cat
{
public:
// three overloaded functions
virtual void func() const
{
cout<<"func default"<<endl;
}
virtual void func(int a) const
{
cout<<"func with int"<<endl;
}
virtual void func(double x) const
{
cout<<"func with double"<<endl;
}
virtual void func(string str) const
{
cout<<"func with string"<<endl;
}
};
class persianCat: public Cat
{
public:
// new redefined functions
virtual void func() const
{
cout<<"new func default"<<endl;
}
virtual void func(nt a_) const
{
cout<<"new func with int"<<endl;
}
};
int main()
{
Cat bai;
persianCat hei;
hei.func(2);
hei.func(2.1);//隐式转换为int
//hei.func("aaa");报错
Cat *p2Cat = &hei;
p2Cat->func();
p2Cat->func(2);
p2Cat->func(2.1);//回退到基类
return 0;
}
通过基类指针 p2Cat
调用 func
函数时,编译器会首先查看 persianCat
类(即指针实际指向的对象的类型)是否有匹配的函数。如果 persianCat
类中有匹配的函数(无论是通过继承还是通过重写),那么就会调用 persianCat
类中的函数。如果没有找到匹配的函数(例如,对于 double
参数的情况),编译器就会回退到基类 Cat
中查找匹配的函数。
1.1.3 动态类型转换
dynamic<A>(B)
动态类型转换,仅仅适用于基类指针指向派生类,否则返回nullptr,转换不成功
例如
class Plant{
protected:
string name;
public:
Plant(string name):name(name){}
virtual Plant* clone() const{
cout<<"Plant "<<name<<" has been cloned."<<endl;
return new Plant(*this);
}
};
class Tree : public Plant{
public:
Tree(string name):Plant(name){
}
virtual Tree* clone() const{
cout<<"Tree "<<name<<" has been cloned."<<endl;
return new Tree(*this);
}
};
int main()
{
Plant* ptrP = new Plant("Skyflower");//换成Tree("skyflower")则有两个输出
Tree* ptrT = dynamic_cast<Tree*>( ptrP->clone() );
//转换失败,基类指针不指向派生类,只有一个输出
if (ptrT) auto ptr=ptrT->clone();//不执行
return 0;
}
1.1.4 成员函数转化成函数指针
尝试传递一个静态成员函数的地址给一个期望函数指针的参数时,编译器可以隐式地将它转换为函数指针
非静态成员函数的地址(成员函数指针)实际上包含了额外的信息,即该函数属于哪个类,以及它在类中的偏移量,与普通的函数指针不兼容,不发生隐式转换。如果需要使用非静态成员函数的函数指针,需要通过对象实例。
typedef int (*FP)(int,int);
class Foo{
public:
int Add(int lhs,int rhs) {…}
static int Mul(int lhs,int rhs) {…}
};
void callback_client(FP f, int a, int b) {
std::cout << f(a,b) << std::endl;
}
void callback_client(int (Foo::*f)(int,int), Foo &foo, int a, int b) //须通过对象实例调用
{
std::cout << (foo.*f)(a,b) + 1000 << std::endl;
}
int main() {
Foo foo;
callback_client(foo.Mul, 2, 3);
callback_client(Foo::Mul, 2, 3);
//callback_client(foo.Add, 2, 3); //error,不发生隐式转换
//callback_client(Foo::Add, 2, 3); //error,不发生隐式转换
callback_client(foo.Add, foo, 2, 3);
callback_client(Foo::Add, foo, 2, 3);
}
explict关键字
阻止隐式转换,若需要转换必须显示声明
1.1.5 对象传参的隐式转换
1.2 引用
声明具名的变量引用,即既存对象或函数的别名。
1.2.1 左值引用的申明
• type &别名 [=左值表达式]
如 int &a=num;
num现在除了有原本的名字num,又多了一个a的别名,在之后的程序中,可以使用a来表示num,修改a就是修改num,因为a和num的内存相同
对比指针
int *p=&num;p指针指向num的地址,*p是num的引用,可以使用*p来表示num,修改*p就是修改num,但不同的是p指针占据额外的内存空间
注意:
引用不是对象,它们不必占用存储
不存在引用的数组
不存在指向引用的指针
不存在引用的引用
1.2.2 左值引用和右值引用
左值:正常的变量,也就是赋值=的左边,可以被赋值
右值:临时对象或即将被销毁的对象,赋值=的右边,不可被赋值,没有持久的存储期,在表达式求值结束后会被销毁。
比如 int a=3;
a是左值,3是右值
左值引用:对有内存空间的正常变量的引用,int &a=num;
右值引用:对右值的引用,比如int&&a=3;注意这里要两个&
1.2.3 引用作为返回值需static
double& f( double x )
{
static double y;
y = sin(x);
return y;
}
从返回值的角度看,引用形参比利用指针更为方便。
//下面的语句,其中假设a的地址是0x78fddc,输出合理的是?
void test(int *&p)
//&p:这是一个取地址操作,传入p的地址,实际上是*p,有点多此一举
{
int a = 2024;
p = &a;//p指向a
cout<<p<<" "<<*p<<endl;//第一个输出p的地址,第二个输出p指向的值
}
int main(void)
{
int *p = NULL;
test(p);//传入p的地址
if(p != NULL)
cout<<"指针p不为NULL"<<endl;
return 0;
}
//0x78fddc 2024
//指针p不为NULL
1.2.4 const修饰引用
首先回顾一下C++的const
C++中,const可与引用搭配
具体可见例子
总而言之采用const int &a=i;时 会为i分配一个临时空间,也就是int k=i;然后再const int &a=k;这里一般会发生隐式转换。
当然const也能修饰成员函数
class Test {
public:
void Test1(int _a)const { //常方法 其中this指针由Test* const--> const Test*const
std::cout<<"Test()const"<<std::endl;
a = _a; //error 常方法不能修改普通成员变量的值
int d = _a; //可以访问a,但不能修改a的值
int e = c; // const函数访问const成员变量
}
private:
int a, b;
const int c; //C++中常非静态成员必须由默认初始化器或初始化器列表初始化
};
1.3 命名空间
如果有多个不同的变量同名,如何区分?
一个好的办法是将其放进不同的命名空间中,C++命名空间提供了一种在大项目中避免名字冲突的方法,在命名空间块内声明的符号或变量被放入一个具名的作用域中,避免这些符号或变量被误认 为其他作用域中的同名符号。
语法
声明命名空间
namespace 命名空间名 { 声明序列 }
使用命名空间
using namespace 命名空间名 ;
using 命名空间名 :: 成员名 ;
随着IDE进步,直接指定使用命名空间中的成员更常用。,即,命名空间名 :: 成员名
#include<iostream>
using namespace std;
//命名空间的声明
namespace sysu {
namespace students {
int collegeCount;
void printColleges();
}
}
void sysu::students::printColleges() {
cout << "Colleges " << collegeCount << endl;
}
int main() {
using namespace sysu::students;//使用sysu的命名空间students
collegeCount = 23;
printColleges();
//IDE上的简略写法
//sysu::students::collegeCount = 23;
//sysu::students::printColleges();
return 0;
}
1.4 输入与输出
1.4.1 输出<<和输入>>
在C语言中,<<和>>是位移运算符
C++形象地将位移运算符用于流对象输入输出操作,
cout << "hello" 的含义是将 hello 字符放入 stdout 输出管道;
int a; cin >> a 的含义是将 stdin 管道输入按 "%d"格式转换后放入变量;
<<被重载为输出运算,左操作数为输出流对象,例如std标准库中的cout。右操作数为任意类型,编译器根据右操作数选择相应的输出格式,例如是double就用"%lf"输出
>>被重载为输入运算,左操作数为输入流对象,例如std标准库的cin,右操作数为左值,编译器根据类型选择相应的格式输入
无论是<<还是>>,表达式均返回左操作数的引用,也就是&cin或&cout,是左值。
1.4.2 操纵符
C语言用\n表示换行,C++也有类似的东西,就是操纵符
比较常用的还有<iomanip>的操纵符
setw(n)
功能:设置输出字段的宽度为n个字符。示例:std::cout << std::setw(10) << 12345 << std::endl;
将输出12345
,并在其后填充空格以达到总宽度为10个字符。
setfill(ch)
功能:设置填充字符为ch。通常与setw
一起使用,以指定当输出数据宽度小于指定宽度时使用的填充字符。示例:std::cout << std::setw(10) << std::setfill('*') << 12345 << std::endl;
将输出*****12345
,其中*
是填充字符。
setprecision(n)
功能:设置浮点数输出的精度为n位小数。示例:std::cout << std::setprecision(4) << 3.14159 << std::endl;
将输出3.142
(四舍五入到4位小数)。
fixed
功能:以定点数的形式输出浮点数,不使用科学计数法。示例:std::cout << std::fixed << 3.14159 << std::endl;
将输出3.141590
(注意末尾的零)。
scientific
功能:以科学计数法的形式输出浮点数。示例:std::cout << std::scientific << 12345.6789 << std::endl;
将输出1.234568e+04
。
setbase(n)
功能:设置数字的基数。n
可以是10(十进制,dec
)、16(十六进制,hex
)或8(八进制,oct
)。示例:std::cout << std::hex << 255 << std::endl;
将输出ff
(255的十六进制表示)。
showbase
功能:在输出数字时显示基数的前缀(如十六进制的前缀0x
)。示例:std::cout << std::showbase << std::hex << 255 << std::endl;
将输出0xff
。
#include<iostream>
using namespace std;//使用std内的cin和cout,如果不加使用声明,需std::cin或std::cout
int main() {
int someInt;
float someFloat;
char someChar;
// fscanf(stdin,"%d%f%c", some...);
cin >> someInt >> someFloat >> someChar;
// fprintf(stdout,"the answer is: %f\n", some...);
cout << "the answer is: " << someInt * someFloat << endl;
return 0;
}
1.5 内存分布和动态分配
静态变量不在栈和堆中分配,而是享有单独的内存空间,而其他变量,如果是自动变量,存在栈中,如果是动态变量,存在堆中。
1.5.1 new
new 类型 初始化
• 分配空间
• 每个对象调用构造器
• 有错误抛出异常
申请数组
new 类型[ ]{初始化列表}(可选)
在堆上分配对象数组并构造初始化。没有初始化列表调用无参构造(默认构造一般是不确定值);常数初始化列表,后面补零;长度小于初始化序列长度,抛出异常 。 返回该类型的指针。
1.5.2 delete
delete p
• 析构对象(delete []p 析构数组)
• 释放空间,相同空间只能释放一次
#include<iostream>
int nums = 2; // 全局变量,存储在数据段
static int num1 = 1; // 全局静态变量,也存储在数据段
int main() {
static int num2 = 2; // 静态局部变量,存储在数据段
int n = 2, m = 3; // 局部变量,存储在栈上
int* p = new int; // 在堆上分配内存,并将指针 p 指向这块内存
*p = n; // 将 n 的值赋给 p 所指向的内存位置
// 这里应该添加代码来释放 p 所指向的内存,防止内存泄漏
// delete p;
return 0;
}
1.5.3 内存泄漏
如果我们申请了一块空间,但是使用完后没有释放,那这块空间就会永远被占用,造成资源的浪费。而计算机的内存空间是有限的,数量多了就会积累下去,使得没有足够的内存给我们使用,这就是内存泄漏。
以下是内存泄漏的几种常见情况
1.6 string类
C 风格字符串是使用 null 字符 '\0' 终止的一维字符数组。因而字符串定义时,必须事先知道保留多大空间存储字符串。
几点注意:
1.strcpy是C语言库的拷贝函数,在cstring库里,拷贝时不需要输入大小,memcpy在string 库里,需要输入大小,
1.7 const和constexpr
C语言的const有两个含义,一个是限定为只读的变量,一个是常量和字面量
#include <iostream>
#include <array>
using namespace std;
void dis_1(const int x){
//错误,x是只读的变量
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}
void dis_2(){
const int x = 5;
array <int,x> myarr{1,2,3,4,5};//此处x既是常量5,又是变量,可用于初始化
cout << myarr[1] << endl;
}
int main()
{
dis_1(5);
dis_2();
}
C++11 标准中,将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义 的场景都使用 const,表达“常量”语义的场景都 使用 constexpr。
1.8 auto
2.类与对象
2.1 类是一种抽象数据类型(ADT)
C++的class和C语言的struct比较类似,但C++的class更加便利。因为C语言的struct只能实现多种基本数据类型的组合构成一个复合的自定义数据类型,但C++的class在此基础上,还可以为本类编写成员函数,实现特定的功能。
2.2 类的语法
class{
public:
...
private:
...
protected:
...
};
关键字
class
和struct
都可以用于定义类,但它们在默认成员访问级别上有所不同。class
默认是private
,而struct
默认是public
。class
和struct
都支持继承,但union
不支持。class
和struct
都可以包含成员函数和特殊成员函数,但union
不能。union
允许你在相同的内存位置存储不同的数据类型,但你必须明确地知道当前存储的是哪个数据类型。
访问说明符
public:公有类型,类内部的成员函数,友元函数与客户端均可调用。
private:私有类型,仅类内部的成员函数或其友元函数。
protected:受保护类型,可以在该类自身以及派生类中被访问,但不能在类的外部被访问。
类成员
数据成员:按声明顺序存储,对齐规则同 C。一 般设计中,数据成员需要隐藏保护,也即是protected或private,需要通过函数成员访问
成员函数:该类所属的函数。公有成员可以被外部使用者访问;私有成员仅内部访问;const 函数成员不能修改数据成员。在内部定义直接编写即可,在外部定义需要再函数的返回类型后面,函数名前添加类名::
使用成员函数:
一般的类:a.func()
类的指针:a->func()
一个简单的例子
//外部定义
class DATE {
public:
void Set(int newYear, int newMonth, int newDay );
……
private:
……
};
void DATE::Set(int newYear, int newMonth, int newDay ) {
month = newMonth;
day = newDay;
year = newYear;
}
//内部定义
class DATE {
public:
void Set(int newYear, int newMonth, int newDay ) {
month = newMonth;
day = newDay;
year = newYear;
}
……
private:
……
};
在DATE.cpp文件开头需要加入预处理命令
#include “DATE.hpp”
这是因为在DATE.cpp中要用到用户自定义的标识符DATE,而它的声明在DATE.hpp中。 在DATE.hpp中,各函数原型是在{}中的。根据标识符的作用域规则,它们的作用范围仅在类声明中,而不包括DATE.cpp。因此在DATE.cpp中需要利用作用域解释运算符“::”来指明这里的函数是类DATE里的成员函数
类
• 是用户定义的数据类型,它表示一个 ADT。在 C++中,它有属性(数据成员) 和能操作数据成员的行为(函数成员)
• 本质上是定义一个数据类型的蓝图,或者是一个模板,说白了就是某个自定义数据类型的定义。
对象
• 类定义的变量称为对象(objects)或者类的实例(instances),结构体定义的变量是传统意义上的变量。
• 自动对象或动态申请的对象的默认初始化值为不确定。
客户端
• 使用类的软件称为客户端代码。
• 客户端代码使用对象的公有方法去处理对象的数据
2.3 类的构造
类有三种构造方式,也就是有三种为构造函数。
构造函数是一种特殊的成员函数,函数名与类名相同,无返回类型,根据传入参数的不同可以分为无参构造函数(不传入参数),有参构造函数(传入非本类的数据类型),拷贝构造函数(传入本类),编译器根据创建类的实例的不同方式调用不同的构造函数。
2.3.1 无参构造函数
无参构造函数,也叫默认构造函数,就是不传入任何参数。一般来说,每一个类都会带一个无参构造函数,要么人为编写,而当用户没有自定义任何构造函数时,则编译会自动生成一个默认构造函数。
#include<iostream>
namespace sysu_cplus {
class Line {
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
}
// 成员函数定义,包括构造函数
sysu_cplus::Line::Line(void) {
std::cout << "Object is being created" << std::endl;
}
void sysu_cplus::Line::setLength( double len ) {
length = len;
}
double sysu_cplus::Line::getLength( void ) {
return length;
}
#include <iostream>
#include "constructor.hpp"
using namespace std;
using namespace sysu_cplus;
// 程序的主函数
int main() {
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
2.3.2 有参构造函数
构造函数也可以带有参数。这样在创建对象时就可使用参数构造对象。
注意:用户一旦定义了构造函数, 编译器就不再自动添加默认构造函数。这时调用无参构造会出错(前提:有参构造函数的参数列表全都不是默认参数)。
#include <iostream>
using namespace std;
class Line {
public:
void setLength( double len ){
length=len;
}
double getLength( ){
return length;
}
Line(); //构造函数
~Line(); //析构函数
private:
double length=0;
};
// 成员函数定义,包括构造函数
Line::Line() {
cout << "Object is being created" << endl;
}
Line::~Line() {
cout << "Object of "<<length<<" is being deleted" << endl;
}
int main( ) {
Line line;
//设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
在有参构造函数中也能使用默认实参,这是C++相比C的新特性,就是可以不提供全部参数而构造类的实例。如果没提供相关的参数,这时会使用默认的参数。注意:类外实现的有参构造函数不能使用默认实参。
class MyClass {
public:
MyClass(int x = 0, double y = 0.0) {
// ...
}
};
MyClass obj1; // 使用默认实参
MyClass obj2(5); // 只提供x的值,y使用默认值
MyClass obj3(5, 3.14); // 提供x和y的值
class MyClass {
public:
MyClass(int x , double y=0 ) {
// ...
}
private:
int x;
double y;
};
MyClass obj1; // error,x没有初始化
MyClass obj2(5); // 只提供x的值,y使用默认值
MyClass obj3(5, 3.14); // 提供x和y的值
class MyClass {
public:
MyClass(int x , double y=0 ) { }
private:
int x;
double y;
};
//类外实现不能使用默认实参
Myclass::Myclass(int x,double y):x(x),y(y){}
2.3.3 成员初始化器
对非静态类型的成员变量来说,除了可以在构造函数体内进行初始化以外,还可以在函数体(花括号前)外进行初始化。 对象初始化分两个阶段:首先按声明顺序初始化成员、然后执行构造函数函数体。
一般来说普通的成员变量怎样初始化都可以,但以下的成员变量只能通过初始化器进行初始化:
1.const修饰的成员变量
2.引用类型的成员变量
3. 派生类构造函数初始化基类的成员变量(此时直接调用基类有参构造函数,见3.3)
4.某一个类成员变量没有无参构造函数,使用初始化器列表可以使用有参构造函数初始化
class Base {
public:
int baseVar;
Base(int value) : baseVar(value) {
// 基类的构造函数初始化列表
}
};
class Derived : public Base {
public:
int derivedVar;
int &nums;
const int a;
// 派生类的构造函数初始化基类成员变量
Derived(int baseValue, int derivedValue,int nums,int a) :
Base(baseValue), derivedVar(derivedValue),nums(nums),a(a) {
// 将基类成员变量在初始化列表中初始化
}
};
//类成员没有无参构造函数
class SubObject {
public:
int value;
SubObject(int v) : value(v) {}
};
class MyClass {
public:
SubObject obj;
MyClass(int value) : obj(value) {
// 使用初始化器列表调用有参构造函数初始化 SubObject 类成员
}
};
注意:必须按照成员变量的声明顺序进行初始化,否则将导致后面的成员变量未定义。
class foo
{
public:
int i ;int j ;
foo(int x):j(x), i(j){} // i值未定义
};
2.3.4 拷贝构造函数
有时我们有一个现成的对象,用拷贝构造函数可以将其里面的成员变量拷贝赋值给另一个新的对象。
拷贝构造函数:在定义语句中用同类型的对象初始化另一个对象
C obja(1,2);
C objb(obja);
语法
用本类型对象引用作第一形式参数。
• 该参数传递方式为对象引用,避免在函数调用过程中生成形参副本
• 该形参一般声明为const,以确保在拷贝构造函数中不修改实参的值
C::C(const C& obj);
每个类都必须有拷贝构造函数:
• 用户可根据自己的需要显式定义拷贝构造函数。
• 若用户未提供,则该类使用由系统提供的缺省拷贝构造函数(可用=default),也可用 =delete 弃置该函数。
• 缺省拷贝构造函数按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制,完成新对象的初始化。即逐一调用成员的拷贝构造,如果成员是基础类型, 则复制值(赋值)。
除了用 声明 C obj2(obj1) 或 new C(obj) 会触发复制构造外, 将一个对象作为实参,以按值调用方式传递给被调函数的形参对象也会隐式调用拷贝构造函数。
void func(C obj)//用obj1来初始化obj,相当于C obj(obj1);
//如果是引用传参就不会调用拷贝赋值
{
...
}
int main(){
C obj1(1,2)
func(obj1);
}
2.3.4.1 浅拷贝
浅复制策略只按声明顺序复制成员的值,不复制指针指向的对象实体。它导致新旧对象成员指针指向同 一块内存。
如果我的类的 有指针类型的成员变量,在进行浅拷贝时只会把拷贝后的对象2的指针成员指向被拷贝的对象1的指针成员,这样就会出现一个问题,如果1的指针成员被delete了,2也会受到影响,可能造成内存泄漏的问题。
2.3.4.2 深拷贝(指针类型的成员变量需考虑)
深复制策略要求成员指针指向的对象也要复制,新对象跟原对象的成员指针不会指向同一块 内存,修改新对象不会改到原对象。也就是说,另外开辟空间来拷贝指针类型成员变量。
以下可以对比浅拷贝和深拷贝
class String{
private:
char* str;
public:
// 浅拷贝策略的拷贝构造函数
String(const String& other) {
// 只是简单地拷贝指针,而不是分配新的内存并拷贝数据
str = other.str;
}
// 深拷贝策略的拷贝构造函数
String(const String& other) {
// 分配新的内存,并将数据从other.str拷贝到str
str = new char[std::strlen(other.str) + 1];
std::strcpy(str, other.str);
}
};
2.3.5 default和delete
2.3.6.1 =default
令编译器为某个类生成特殊成员函数或比较运算符的显式指令。(不能为其他成员函数代写)
• 当我们声明有参构造函数时,编译器不会创建默认构造函数,如果我们希望编译器依然创建默认构造函数,就需要使用default。
• =default 必须在函数声明后, 函数不能再自定义实现,而是由编译器帮我们自动代写,这样后面我们也可以调用相应的函数了。
• 特殊成员函数包括:
➢ 默认构造函数:T()
➢ 析构函数:~T()
➢ 复制构造函数:T(const T&)(浅拷贝代写)
➢ 赋值运算:operator=(const T&)
实际上,除了默认构造函数在有参构造函数定义后就不再自动生成外,析构函数,复制构造函数,赋值运算都是可以自动生成的,如果你没有编写的话。但一般用=default可以增强程序的可读性。
2.3.6.2 =delete
如果我们希望某个函数不能被调用,可以采用=delete的方式,声明它是“弃置的”,在这时调用相应函数就会出错。
注意:删除的成员函数必须在类内声明为=delete,删除的函数定义必须是函数的首次声明。
class MyClass {
public:
// 构造函数
MyClass() { /* ... */ }
// 一个被删除的成员函数
void someFunction() = delete;
// 其他成员函数
void anotherFunction() { /* ... */ }
};
int main() {
MyClass obj;
// obj.someFunction(); // 这行代码会导致编译错误,因为someFunction被删除了
obj.anotherFunction(); // 这行代码是合法的
return 0;
}
2.4 类的析构
类的析构函数是类的一种特殊的成员函数,它会在对象被释放前执行,也就是释放某一个类的实例内部的所有数据成员变量。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号 (~)作为前缀,它不会返回任何值,也不能带有任何参数。
注意:析构函数也不能直接调用,对于自动类型的对象实例,当程序结束编译器会自动调用析构函数(与构造顺序相反,即最晚构造的第一个析构),而对于动态分配的对象实例,当显式delete时会调用析构函数。
#include <iostream>
using namespace std;
class Line {
public:
void setLength( double len ){
length=len;
}
double getLength( ){
return length;
}
Line(); //构造函数
~Line(); //析构函数
private:
double length=0;
};
// 成员函数定义,包括构造函数
Line::Line() {
cout << "Object is being created" << endl;
}
Line::~Line() {
cout << "Object of "<<length<<" is being deleted" << endl;
}
int main( ) {
Line line;
//设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
Line *p=new Line;
delete p;
return 0;
}
//Object is being created
//Length of line : 6
//Object is being created
//Object of 0 is being deleted
//Object of 6 is being deleted
应当注意的是,如果不显示delete,析构函数只能销毁栈上创建的对象,而不会对堆上创建的对象进行析构,参考以下例子
class A {
public:
int x;
A() = default;
A(int x):x(x) {}
A(const A& that) = default;
~A() = delete;
};
int main() {
A a;
A* pa = new A;
//程序结束,a调用析构函数销毁,但因为析构函数被声明为弃置的,编译出错
//导致编译不通过的语句为~A() = delete;和A a;
//pa不会被销毁,不调用析构函数,会导致内存泄漏,除非delete pa
return 0;
}
2.5 this指针
在 C++ 中,每一个对象都能通过 this 指 针来访问自己的地址。this 指针是所有成 员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。也就是自身调用某一函数,提供接口,因为根据类的成员函数调用时必须是a.func(),a->func(),this指针就是想对自身调用某一函数时的a。
同时this指针也可用来区分不同类实例中的数据成员或函数成员,因为我们类的数据成员和成员函数都是同名的。比如base类的数据成员叫data,在编写成员函数void cal(base p)时,
this->data就是调用的实例变量的data,p->data就是p内的data。
static 成员不能使用 this,应使用 类名::成员
友元函数没有 this 指针,因为友元不是类的成员。只有动态成员函数才有 this 指针。
当返回本类对象的引用时,需使用this指针
#include <iostream>
using namespace std;
class Box {
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0) {
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume() {
return length * breadth * height;
}
int compare(Box box) {
return this->Volume() > box.Volume();
}
//返回本类对象的引用时,使用this指针
&BOX ReturnBox(){
return *this;
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void) {
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
if(Box1.compare(Box2)) {
cout << "Box2 is smaller than Box1" <<endl;
} else {
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}
2.6 static修饰的类成员
静态(static)成员是类的组成部分但不是任何对象的组成部分
在数据成员的类型前加保留字 static声明静态数据成员;在成员函数的返回类型前加保留字static声明静态成员函数
static成员遵循正常的公有/私有访问规则。 C++程序中,如果访问控制允许的话,可在类作用域外直接(不通过对象)访问静态成员(需加上类名和::)
静态数据成员具有静态(全局)生存期,是类的所有对象共享的存储空间,是整个类 的所有对象的属性,而不是某个对象的属性,也就是所有类的实例共同享有的成员。
类的静态成员函数没有this指针,静态成员函数不能直接访问类的非静态成员,只能直接访问类的静态数据或函数成员
• 静态数据成员的初始化:必须在类定义体的外部再定义一次,且恰好一次,且此时不能再用static修饰。注意:不可用构造函数初始化。
class DATE // DATE.h
{
public:
DATE( int =2000, int =1, int = 1);
static void getCount( );
void Set( int, int, int);
int getMonth() const;
int getDay() const;
int getYear() const;
void Print() const;
void Increment();
void Decrement();
private:
int month;
int day;
int year;
static int count;//这里相当于对天数进行计数,因此是所有类的对象的共同属性
//有一个对象被创建,天数就递增
};
//DATE.cpp StaticMember
int DATE::count = 0; //必须在类定义体的外部再定义一次,且不带static
DATE::DATE( int initYear, int initMonth, int initDay )
{
year = initYear;
month = initMonth;
day = initDay;
count++;
}
void DATE::getCount()
{
cout << "There are " << count << " objects now" << endl;
}
2.7 运算符重载
对于基本类型的变量来说,使用运算符(+,-,/等)可以完成不同的运算,但由于类中封装了多种不同的基本类型或其他类的类型,基本运算符是失效的,如果我们依旧希望对于该类的对象能够使用基本运算符,需要定义运算符重载函数。
重载的运算符可以理解为带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算 符符号构成的。
基本语法就是
//以+的重载为例子
class A{
public:
A operator+(A& b){
...
return A(...);
}//返回一个运算后的A的对象
A& operator+(A& b){
...
return *this;
}//返回this指针
};
2.7.1 不可重载的运算符
2.7.2 双目、单目运算符
常见双目、单目运算符的重载函数声明如下
注意类内定义和类外定义运算符重载函数的参数列表不同
类内:只需要除被调用对象以外的其他对象进行传参,因为类内的成员函数都拥有this指针作为隐藏参数
类外:此时不是成员函数,不提供this指针,因此需要额外一个参数
运算符 && 与 || 的重载失去短路求值。
2.7.3 自增/自减运算符
注意区分前缀自增/自减和后缀自增/自减的重载:
前缀自增/自减返回的是运算后的被调用对象,因此返回类型为T&,返回*this
后缀自增/自减返回的是运算前的被调用对象的副本,返回类型为T,尽管在函数体内完成了被调用对象的计算。需预先对运算前的对象进行拷贝,最后返回拷贝的副本,同时参数列表需要添加int进行声明,此处的int不被使用。
2.7.4 赋值运算符
所有的赋值运算符都返回引用类型,因此都需要this指针,注意简单赋值运算符不能类外定义,因此简单赋值运算符只能作为类的成员函数进行重载。
赋值运算符有多种语义,这里仅介绍浅拷贝和深拷贝两种
注意赋值运算符重载函数和拷贝构造函数的调用区别
拷贝构造函数是用于创建一个新的对象作为现有对象的副本。它在对象创建时调用,而不是在对象已经存在时调用。
赋值运算符重载函数是用于将一个已存在的对象的值赋给另一个已存在的对象。它在对象已经存在时调用,而不是在创建对象时调用。
T a=b;//调用拷贝构造
T a;
a=b;//调用赋值运算符重载函数
2.7.4.1 浅拷贝
对于不含指针类型的成员变量已足够
2.7.4.2 深拷贝
1.检查自赋值if(this!=&other)
2.检查动态内存分配大小,若已经分配了内存,需先delete后重新分配。因为拷贝构造函数是在对象创建时调用,因此可以不用删除已经分配的内存,也不需要检查自拷贝。而赋值运算符重载是在对象已经存在时调用,因此需要先释放分配的内存。
class Matrix {
public:
Matrix() :rows(0), cols(0), data(NULL) {}
Matrix(int rows, int cols) :rows(rows), cols(cols) {
data = new int[rows * cols];
}
Matrix(const Matrix& other) {
this->rows = other.rows;
this->cols = other.cols;
this->data = new int[rows * cols];
memcpy(this->data, other.data, rows * cols * sizeof(int));
}
~Matrix() {
delete[] data;
}
Matrix& operator=(const Matrix& other);
private:
int rows, cols;
int* data;
};
Matrix& Matrix::operator=(const Matrix& other) {
if (this != &other)//检查自赋值
{
this->rows = other.rows;
this->cols = other.cols;
delete[] data;//先删除已经分配的内存,防止内存泄漏
this->data = new int[rows * cols];
memcpy(this->data, other.data, rows * cols * sizeof(int));//数据拷贝
}
return *this;
}
2.7.5 下标运算符重载
class IntArray {
int *a;
int n;
public:
IntArray(int n=1):n(n) {…}
IntArray(const IntArray& other) {…}
~IntArray() {…}
int& operator[](int i) {
if (i>=0 && i < n) {
return a[i];
}
throw std::out_of_range("out of range");
}
const int& operator[](int i) const{
if (i>=0 && i < n) {
return a[i];
}
throw std::out_of_range("out of range");
}
void print() const {…
}
};
int main() {
IntArray a(4);
for (int i=0;i<4;i++)
a[i]=i+1;//调用非const函数
a.print();
const IntArray b=a;//const对象调用const函数
cout << "b[0] = " << b[0] <<endl;
return 0;
}
//1 2 3 4
//b[0]=1
//1 2 3 4
2.7.6 函数调用运算符重载
如果类T中对函数调用运算符进行了重载,那么T就是函数类,生成的对象就是函数对象。
2.7.7 类外定义运算符重载函数
我们知道,如果类内定义运算符重载,一般来说操作数的顺序是不可交换的
比如下面的这个例子
class Integer{
private:
int num;
public:
Integer(int a):num(a){}
Integer operator+(int a){
return Integer(num+a);
}
};
Integer a(5);
Integer c=a+4;//左操作数只能为Integer类型
如果想实现c=4+a这个例子,就必须在类外定义运算符重载函数
class Integer{
public:
int num;
public:
Integer(int a):num(a){}
Integer operator+(int a){
return Integer(num+a);
}
};
Integer operator+(const Integer& a, const Integer& b) {
return Integer(a.num+b.num);
}
Integer a(5);
Integer c=4+a;//可交换
//传入参数时能发生隐式转换,4转换为Integer类型
类外定义的运算符重载函数虽然可交换,但也有个问题,那就是不能访问私有数据,要访问就必须要设置为公有类型,这对面向对象设计的信息封装带来了问题。但是友元函数解决了这个问题。
2.7.8 友元函数
如果一个函数在类外定义,但在类内有声明为friend,那就可以访问该类的私有数据。
当然我们也可以将其他类的成员函数声明为友元函数
类似的还有友元类
但也不是所有的运算符都可以声明为友元函数的
最常见被声明为友元函数的就是输入输出运算符的重载,注意写法和返回的对象。
#include<iostream>
using namespace std;
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction() : numerator(0), denominator(1) {}
Fraction(int num, int denom) : numerator(num), denominator(denom) {
simplify();
}
friend bool valid(const Fraction& f);
friend istream& operator>>(istream& is, Fraction& f);
friend ostream& operator<<(ostream& out, const Fraction& f);
};
bool valid(const Fraction& f){
if(f.denominator==0)
return false;
else
return true;
}
istream& operator>>(istream& is, Fraction& f){
is>>f.numerator;
is>>f.denominator;
return is;
}
ostream& operator<<(ostream& out, const Fraction& f){
out<<f.numerator<<"/"<<f.denominator;
return out;
}
当重载的函数多了时,调用时应根据重载协议进行匹配。
3.类的继承与派生
3.1 继承方式与访问控制
1.单继承(父类为1个);
2.多重继承(父类为2个即以上);
3.菱形继承(两次以上重复继承祖先类)
注意菱形继承需在祖先类(A)的成员函数前天街virtual关键字(防止重复继承导致基类副本重复构造)
private:成员不可访问
public:可在外部调用公有成员(客户端调用)
protected:仅可在类定义内部调用公有成员,外部不可调用
3.2 构造顺序与析构顺序
构造顺序
• 虚基类的构造函数;
• 普通基类的构造函数,多个基类则按派生类声明时列出的次序、从左到右调用;
• 对象成员的构造函数(即该类对象的成员中含有其他类的实例),按类声明中对象成员出现的次序调用;
• 派生类的构造函数
析构顺序:与构造相反
3.3 派生类构造函数
如果带参数,且需要调用基类含参构造函数初始化变量,则只能通过初始化列表调用,否则将调用基类的默认构造函数,即无参构造函数
3.4 派生类成员函数(仅考虑与基类同名的情况)
3.4.1重载(overload)
同名函数可以有不同参数类型
条件:
一个类中出现两个同名函数,但参数类型不同(派生类与基类不存在重载关系)
3.4.2隐藏(overwrite)
基类函数不可使用,屏蔽基类函数定义
条件(满足之一):
1.派生类函数与基类函数同名,参数不同
2.派生类函数与基类函数同名,参数相同,但基类没有virtual关键字
如需调用基类被隐藏的函数,需添加Base::
3.4.3覆盖(override)
修改基类同名成员函数的定义
条件(须全满足):
1.基类或非直接基类,至少有一个成员函数被 virtual 修饰
2.派生类虚函数必须与基类虚函数有同样签名,即函数名,参数类型,顺序和数量都必须相同。
3.5 恢复访问控制
using Base::成员名,使基类的成员在派生类中不再遵循继承控制方式,比如private继承的,可以使用该方法使基类成员被公有继承
3.6 类型兼容
1.可以把公有派生类对象赋值给其继承的基类乃至祖先类,即向上兼容,可以把公有派生类当作基类使用,反之不可
2.基类指针或引用可指向或引用公有派生类,即向下兼容,基类对象不可指向或引用公有派生类,即向下不兼容
3.private,protected不具有类型兼容性
备注:虽然基类指针或引用可以直接指向或引用公有派生类,但向下转换通常是不安全的,一般采用dynamic_cast<派生类指针/引用>(p),将基类指针/引用p显示转换为派生类的指针/引用。
当然,派生类指针/引用也可以指向/引用另一个派生类的指针/引用,依然是使用dynamic_cast,这是侧向转换
3.7 多重继承与虚基类
基本语法
class derived:public base1,public base2...{}
虚基类:virtual public base1,普通基类与虚基类之间的唯一区别只有在派生类重复继承了某一基类时才表现出来,也即是菱形继承,virtual关键字保证虚基类的副本只初始化一次,且构造函数调用时先调用虚基类再调用普通基类。
4.类的多态
多态的实现有三种方式:函数重载,方法覆盖(虚函数),泛型(模版)
多态其实是一个同名函数的多种形态,不同的参数列表(重载),不同的实现方法(虚函数),不同的数据类型(泛型)。
4.1绑定方式
不同的多态方法通过不同的模式确定应该调用哪个形态的函数,有静态绑定和动态绑定两种方式
4.1.1静态绑定(也叫静态联编)
编译时确定,相关的变量类型为静态类型
4.1.2动态绑定
运行时才确定,相关的变量类型是动态类型
动态类型
1.多态类型指针或引用,即基类指针或引用指向派生类指针或引用
2.访问的成员是虚函数
class Fruit{
public:
virtual void say() {
printf("I'm a fruit!\n");
}
};
class Apple : public Fruit {
public:
void say() {
printf("I'm an apple!\n");
}
};
int main(){
Apple a;
Fruit *fPtr = &a;
fPtr->say();
}
4.2 函数重载
定义多个不同的参数列表的同名函数,使得该函数可以适用于多种方式的调用。
class Apple{ /*可以随意填入成员*/ };
class Banana{ /*可以随意填入成员*/ };
class Cherry{ /*可以随意填入成员*/ };
void say(Apple fruit) {printf("I'm an apple!\n");}
void say(Banana fruit){printf("I'm a banana!\n");}
void say(Cherry fruit){printf("I'm a cherry!\n");}
int main(){
Apple a;
Banana b;
Cherry c;
say(a);
say(b);
say(c);
}
4.3 虚函数
相同函数签名的成员函数前添加关键字virtual。
4.3.1作用:实现覆盖(override)
覆盖(override)
在派生类中修改基类成员函数(相同函数签名)的定义
在公有继承层次中,对派生类的虚函数进行重定义。当派生类的对象使用它基类的指针(或引用)调用虚函数时,将调用该对象(即派生类)的成员函数。
简单来说就是虚函数就是派生类与基类有同名的成员函数,且参数列表都相同(即拥有相同的函数签名),因为如果不同基类成员函数就被隐藏了。同时基类的成员函数前有virtual关键字,这使得派生类可以对成员函数进行重新定义,实现不同的类调用同一个函数出现不同结果,也就是多态。
注意:隐藏(overwirte)没有virtual关键字
派生类对象指针/引用进行向上类型转换成基类基类/引用
• 覆盖:调用该对象的虚函数
• 隐藏:调用基类的函数
4.3.2虚函数性质
一旦为虚,永远为虚
基类是虚函数,则其直接或间接派生类的相同函数签名的函数都是虚函数,都可被重新定义。
不能声明为虚函数:
普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。
4.3.3抽象类与纯虚函数
抽象类其实就是一种特殊的类,与类模版比较类似,但不是对数据类型进行泛化,而是对该类的实现方法进行泛化。比如有个类Student是抽象类,其中成员函数有get_Grade,获取所在年级,go_to_school,上学,可以派生出小学生,中学生,大学生等具体类。其中的继承并覆盖(override)的成员函数就是抽象类中的纯虚函数
纯虚函数
class A{
public:
virtual void func()=0;
}
纯虚函数一般不给出定义,除非是析构函数
抽象类
定义或继承了至少一个纯虚函数的类
• 抽象类不能被实例化,不能定义一个抽象类变量
• 抽象类的子类是抽象类,除非所有继承来的纯虚函数都被覆盖(override)重新编写
• 能仅能作为基类指针或引用,只带自己派生类对象
• 不能被显式转为抽象类对象。
4.3.4虚析构
析构函数不可继承,但若基类声明其析构函数为 virtual,则派生的析构函数始终覆盖它。 这使得可以通过指向基类的指针 delete 动态分配的多态类型对象
任何包含虚函数的基类的析构函数必须为公开且虚,或受保护且非虚。
4.4 RTTI 运行时类型识别
运行时确定实际的对象类型。在多态中指使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型,在运行时将一个多态指针转换为其实际指向对象的类型的过程。
4.4.1 typeid操作符
返回指针和引用所指的实际类型
语法:typeid(类型)或typeid(表达式)两种
4.4.2 dynamic_cast操作符
语法:
A*a=dynamic_cast<A*>b;b是非A类的实例变量
若转型成功,则 dynamic_cast 返回新类型类型的值。
若转型失败
• 且 新类型 是指针类型,则它返回该类型的空指针。
• 且 新类型 是引用类型,则它抛出与类型 std::bad_cast 的处理块匹配的异常。
int main() {
//向上
Animal *a = new Donald();
//向下
Duck *d = dynamic_cast<Duck*>(a);
//侧向
Actor *c = dynamic_cast<Actor*>(d);
delete c;
}
常见转化失败的情况
1.基类指针到派生类指针的转换,但基类指针事先不指向派生类对象或指针
class Plant{
protected:
string name;
public:
/* 此处已省略给name赋值的构造函数 */
virtual Plant* clone() const{
cout<<"Plant "<<name<<" has been cloned."<<endl;
return new Plant(*this);
}
};
class Tree : public Plant{
public:
/* 此处已省略构造函数 */
virtual Tree* clone() const{
cout<<"Tree "<<name<<" has been cloned."<<endl;
return new Tree(*this);
}
};
Plant* ptrP = new Plant("Skyflower");
Tree* ptrT = dynamic_cast<Tree*>( ptrP->clone() );
//此处转换失败,因为返回的Plant*没有指向派生类对象或指针
if (ptrT)
auto ptr=ptrT->clone();
2.基类引用到派生类引用的转换,但基类引用实际上并不绑定到派生类对象
这种转换在编译时可能不会报错,但在运行时如果基类引用不是派生类对象的别名,则转换会抛出std::bad_cast
异常
class Plant{
protected:
string name;
public:
/* 此处已省略给name赋值的构造函数 */
virtual Plant* clone() const{
cout<<"Plant "<<name<<" has been cloned."<<endl;
return new Plant(*this);
}
};
class Tree : public Plant{
public:
/* 此处已省略构造函数 */
virtual Tree* clone() const{
cout<<"Tree "<<name<<" has been cloned."<<endl;
return new Tree(*this);
}
};
Plant& plantRef = *new Plant();
Tree& treeRef = dynamic_cast<Tree&>(plantRef); // 运行时错误,抛出std::bad_cast
3.跨继承体系的转换:
如果两个类之间没有继承关系,或者继承关系不是从基类到派生类的直接路径,那么尝试在这两个类之间进行dynamic_cast
转换也是非法的。
class Animal {};
class Plant {};
Animal* animalPtr = new Animal();
Plant* plantPtr = dynamic_cast<Plant*>(animalPtr); // 非法转换,无继承关系,返回nullptr
4.对非多态类型的转换:
如果基类没有虚函数(因此不包含虚函数表),则不能对其进行dynamic_cast
转换,因为dynamic_cast
依赖于运行时类型信息(RTTI),这些信息是通过虚函数表提供的。
class NonPolymorphic {};
class Derived : public NonPolymorphic {};
NonPolymorphic* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 非法转换,因为NonPolymorphic不是多态类型
4.5 final关键字
指定某个虚函数不能在子类中被覆盖,或者某个类不能被子类继承
struct Base {
virtual void foo();
};
struct A : Base {
void foo() final; // Base::foo 被覆盖而 A::foo 是最终覆盖函数
void bar() final; // 错误: bar 不能为 final 因为它非虚
};
struct B final : A { // struct B 为 final
void foo() override; // 错误:foo 不能被覆盖,因为它在 A 中是 final
};
struct C : B { // 错误:B 为 final
};
4.6 函数模版与类模版
4.6.1 函数模版
int main()
{
std::string s1("rabbit"), s2("bear");
int iv1 = 3, iv2 = 5;
double dv1 = 2.8, dv2 = 8.5;
// 调用函数模板的实例Swap(string&, string&)
Swap(s1, s2);
// 调用函数模板的实例Swap(int&, int&)
Swap(iv1, iv2);
// 调用Swap的实例Swap(double&, double&)
Swap(dv1, dv2);
}
模版函数调用过程:
1.参数推断,编译器确定相应模版形参。
2.函数模版的实例化。创建函数模版的一个实例,例如上面创建的swap(string&,string&)。此过程不进行隐式转换。
特化
如果相对函数模版的某些特定的数据类型进行不同的操作,在正常的模板下面接着编写代码,写一个空 的template<>然后写个具体的函数代码来 补充
template <typename T>
void Swap( T& v1, T& v2)
{
T temp;
temp = v1;
v1 = v2;
v2 = temp;
}
template <>
void Swap( int & v1, int & v2)//int类型的特化
//当传入的实参类型是int类型,就执行模板的特化部分,而非int类型执行正常的模板推断
{
int temp;
temp = v1;
v1 = v2;
v2 = temp;
v1 += 10;
v2 += 10;
}
4.6.2 函数调用的静态绑定规则
匹配顺序:函数调用的静态绑定规则(重载协议):
1. 形参类型与调用实参相同的同名非模版函数(正常函数)
2. 同名的函数模板实例化的一个函数实例与调用的实参类型相同(首先匹配特化)
3. 对函数调用的实参隐式类型转换后与非模板函数再次匹配(模版函数不支持隐式转换)
4. 提示编译错误
// 函数模板demoPrint
template <typename T>
void demoPrint(const T v1, const T v2){
cout << "the first version of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}
// 函数模板demoPrint的指定特殊
template <>
void demoPrint(const char v1, const char v2){
cout << "the specify special of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}
// 函数模板demoPrint重载的函数模板
template <typename T>
void demoPrint(const T v){
cout << "the second version of demoPrint()" << endl;
cout << "the argument: " << v << endl;
}
// 非函数模板demoPrint
void demoPrint(const double v1, const double v2){
cout << "the nonfunctional template version of demoPrint()" << endl;
cout << "the arguments: " << v1 << " " << v2 << endl;
}
/* 函数调用 */
string s1("rabbit"), s2("bear");
char c1('k'), c2('b');
int iv1 = 3, iv2 = 5;
double dv1 = 2.8, dv2 = 8.5;
// 调用第一个函数模板
demoPrint(iv1, iv2);
// 调用第一个函数模板的指定特殊
demoPrint(c1, c2);
// 调用第二个函数模板
demoPrint(iv1);
// 调用非函数模板
demoPrint(dv1, dv2);
// 隐式转换后调用非函数模板
demoPrint(iv1, dv2);
4.6.3 类模版
语法
模版形参
类型模版形参,泛型数据,编译器根据调用方式自动推断参数类型
非类型模版形参,相当于模板内部的常量,形式上类似于普通的函数形参,由实例化的常量值确定
如:Stack<int,10> stk,10个元素大小的栈
打印函数
/* 非模板形参实例1 */
template <typename T, int N>
void printValues(T (&arr)[N]) {
for (int i = 0; i != N; ++i)
cout<< arr[i] << endl;
}
int main()
{
int intArr[6] = {1, 2, 3, 4, 5, 6};
double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
// 生成函数实例printValues(int (&) [6])
printValues(intArr);
// 生成函数实例printValues(double (&) [4])
printValues(dblArr);
return 0;
}
/* 非模板形参实例2 */
template <typename T, int N>
void printValues(T (*arr)[N]) {
for (int i = 0; i != N; ++i)
cout<< (*arr)[i] << endl;
}
int main()
{
int intArr[6] = {1, 2, 3, 4, 5, 6};
double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
// 生成函数实例printValues(int (&) [6])
printValues(&intArr);
// 生成函数实例printValues(double (&) [4])
printValues(&dblArr);
return 0;
}
/* 不使用非模板形参实现 */
template <typename T>
void printValues(T* arr, int N) {
for (int i =0; i != N; ++i)
cout<< arr[i] << endl;
}
int main()
{
int intArr[6] = {1, 2, 3, 4, 5, 6};
double dblArr[4] = {1.2, 2.3, 3.4, 4.5};
// 生成函数实例printValues(int*, 6)
printValues(intArr, 6);
// 生成函数实例printValues(double*, 4)
printValues(dblArr, 4);
return 0;
}
模版类:类模版实例后的产物。
• 类模板名 < 模板实参表 >
模板实参是一个实际类型,如int,double等
如:Stack<int> stk,其中Stack类为类模版
类外定义类模版的成员函数
若此成员函数中有模板参数,则还需在函数体外进行模板声明
如果函数是以通用类型为返回类型,则要在函数名后加上“<T>”,派生类时,如果父类为类模版,则也需加上“<T>”
template <class T>
class Test
{
……
}
template<class T>
void Test<T>::print(){}
template<class T>
Test<T>::Test(T k):i(k){
n=k;
cnt++;
}//类外构造函数
template<class T>
T Test<T>::operator+(T x){
return n + x;
}//类外重载函数
template<class T>
int Test<T>::cnt=0;//类外初始化静态成员
class Derived:public Test<T>{
}
5.STL库
5.1迭代器:
迭代器与指针类似,指向的是容器中的某一位置的特殊的类
5.1.1迭代器类别
类别 | 功能 | 支持的操作 | 备注 |
输入迭代器 | 读 | const * const -> | 解引用(*)的结果只能作为右值(常量) |
输出迭代器 | 写 | * -> | 解引用(*)的结果可以作为左值(变量)、右值 |
正向迭代器 | 读/写 | ++,==, != | 正向遍历 |
双向迭代器 | 读/写 | ++,- -,==,!= | 正向、反向均可 |
随机访问迭代器 | 读/写 | ++,--,+,-,+=,-= ==,!=,<=,<,>,>= [] | iter[n]等价于 *(iter+n) |
容器类别 | 读/写 | 迭代器类别 | 备注 |
array | r/w | random | 无专门迭代器类别 |
vector | r/w | random | iterator,const_iterator |
deque | r/w | random | iterator,const_iterator |
list | r/w | bidirectional(双向) | iterator,const_iterator |
set/multiset | r | bidirectional | const_iterator |
map/multimap | r | bidirectional | iterator (指向pair<const Key, T>,T可修改,Key不可修改)和const_iterator |
forward_list | r/w | forward | iterator,const_iterator |
注意:.cbegin()获取const,.begin()获取非const
5.1.1迭代器的遍历
int main() {
auto _begin = ils.begin();
auto _end = ils.end();
for (auto it = _begin; it != _end; it++)
{
auto x = *it;
cout << x << ",";
}
}
for ( const auto& x:container )(C++11后)
条件(满足其中一条)
• 是 array
• 或 有 begin() 和 end() 成员函数的对象
• 或 一个花括号初始化器列表
其中, const 和 引用 都是可选的
5.2常见算法及其语法
5.2.1 sort
sort(a.begin(),a.end()),注意为左开右闭区间
如果添加lambda表达式,即sort(a.begin(),a.end(),lambda表达式),可以实现按lambda表达式进行排序,比如降序
sort(a.begin(),a.end(),[](int a,int b){return a>b;});
//当lambda表达式为真时,保持a,b位置不变
注意:获取的迭代器指向容器的指定位置,对容器进行排序会改变该位置的值
5.2.2 upper_bound和lower_bound
lower_bound(a.begin(),a.end(),k);//返回>=k的第一个数对应的位置的迭代器
upper_bound(a.begin(),a.end(),k);//返回>k的第一个数对应的位置的迭代器
lower_bound(a.begin(),a.end(),k,[](int a,int b){return a<b;});
//a是k,lambda表达式为真时返回迭代器,即>=k,因为是lowwer_bound,自动补上了=
upper_bound(a.begin(),a.end(),k,[](int a,int b){return a<b;});
//a是k,lambda表达式为真时返回迭代器,即>k
6.lambda函数
语法
[捕获列表](参数列表) -> 返回类型 { 函数体 }
其中,捕获列表是函数体内部需要使用的变量,传入的是变量名,如果要对捕获变量进行修改,应当使用&引用形式捕获,否则将报错
lambda函数作为参数
比如
sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });
这是降序排列,sort在遍历元素时会调用lambda函数,如果为真,则保存a,b位置不变,否则就将a排在b后面,也就是降序排列
注意
在 C++11 和 C++14 标准中,auto
关键字在函数参数类型中并不被允许用于 Lambda 表达式的参数类型推断。这是因为 auto
的用途是自动类型推导,而 Lambda 表达式的参数类型必须在编译时确定,因为它们是 Lambda 表达式的静态部分。
从 C++17 开始,Lambda 表达式允许使用 auto
关键字进行参数类型推导,但你必须使用 auto
关键字和尾随返回类型(trailing return type)
int main() {
using namespace std;
//vector v = { 1, 2, 3, 4 };
vector<int> v = { 1, 2, 3, 4 };
//auto res = accumulate(v.begin(), v.end(), 1, [](auto i, auto j) { return i * j; }); //C++11,C++14 error
auto res = accumulate(v.begin(), v.end(), 1, [](decltype(v[0]) i, decltype(v[0]) j) { return i * j; });
//decltype编译时推断表达式的类型
cout << res;
//常用lambda表达式
accumulate(a.begin(),a.end())//计算[a.begin(),a.end())区间和
accumulate(a.begin(),a.end(),1,[](int i,int j){return pow(i,j);}
//初值1,作为i,以lambda函数的形式计算,遍历的每个元素作为j,上一次的计算结果作为i
//比如[1,2,3],第一次1^1=1,第二次1^2=1,第三次1^3=1;
lower_bound(a.begin(),a.end(),k,[](int a,int b){return a<b;})
//>=k,lower_bound自动补上=
upper_bound(a.begin(),a.end(),k,[](int a,int b){return a<b;})//>k
lower_bound(a.begin(),a.end(),k,[](int a,int b){return a>b;});//error
//虽然期望返回第一个小于k的数,但由于算法是二分查找实现的,而且是升序的
//lambda表达式比较顺序与排序相反,导致错误
upper_bound(a.begin(),a.end(),k,[](int a,int b){return a>b;});//error同理
//a是k,遍历的每个值为b,当表达式结果为真时,返回该位置迭代器
//通常默认是大于k,如果没有lambda表达式
}
7.异常处理
采用结构化方法对程序的运行时错误进行显式管理:
• 处理的是可预料的错误或特殊事件。
• 将程序中的正常处理代码与异常处理代码显式区别开来,提高程序的可读性。
7.1基本思想
结构化定义异常
• 将异常种类定义为树状结构
结构化处理异常(异常检测与异常处理分离)
• 异常检测部分检测到异常的存在时,抛出一个异常对象给异常处理代码。
• 集中捕获异常对象,再处理异常。
• 通过该异常对象,独立开发的异常检测部分和异常处理部分能够就程序执行期间所出现的异常情况进行通信
7.2语法
案例
#include <cstdio>
#include <cerrno>
int main () {
FILE * pf;
try {
pf = fopen ("unexist.txt", "rb");
if (pf == NULL) throw errno;
//errno是一个全局变量,用于报告库函数的错误,类型通常为int
//fopen调用失败,errno会被设置为非0值,不同的值表示不同的错误
fclose (pf);
}
catch (int errnum)//errnum存储从 throw 语句中抛出的值
//catch按顺序根据类型匹配抛出的对象并进行捕获
//抛出异常后立刻进行相关异常的捕获,没有成功捕获会调用std::terminate()函数,程序异常终止
{
if (errnum!=2)
fclose (pf);//errno为2时,表示没有相关文件和目录
else
perror("unexist.txt");
}
return 0;
}
注意:
1.try 块中一旦遇到异常, 便终止执行,不再进行try内后续部分的执行。
2.try 块后至少有一个异常申明。
3.异常申明,异常对象建议用引用捕获,否则生成一个副本,导致异常捕获错误。如果是基类引用捕获异常派生类,虚函数才能产生多态。
4.子类必须优先基类catch。使用catch(…)处理前面未截获的其他异常,放在最后。
5.限定函数异常,void func() throw(int,float),限定只能抛出int或float类型的异常,如果抛出其他类型将调用std::abort(),程序强制终止
7.3常用标准库异常类
异常基类,在头文件<exception>
class exception {
public:
exception () noexcept;
//noexcept用于指定一个函数是否可能抛出异常,保证在执行过程中不会抛出任何类型的异常
//如果尝试抛出一个异常,立即调用std::terminate()函数终止
exception (const exception&) noexcept;
exception& operator= (const exception&) noexcept;
virtual ~exception();//允许通过基类指针或引用来安全地删除派生类对象
//如果没有虚析构函数,当通过基类指针或引用删除派生类对象时,只会调用基类析构函数
virtual const char* what() const noexcept;//允许派生类override返回更具体的信息
}
1. std::bad_alloc
由new
操作符在无法分配所请求的内存时抛出的异常。这通常发生在堆内存不足时。
这个异常类定义在头文件<new>
中。
2. std::runtime_error
表示在运行时发生的,无法预料的异常。这些异常通常是由于程序执行期间出现的错误条件导致的,而不是由于程序员的逻辑错误。例如,文件无法打开、无效的输入数据等都可能导致std::runtime_error
异常。
这个异常类定义在头文件<stdexcept>
中,并且是std::exception
的直接派生类。
3. std::logic_error
表示程序逻辑错误的异常。这些异常通常是由于程序员的编码错误或设计缺陷导致的。例如,尝试访问数组的有效范围之外的元素、尝试除以零、在不应该为空的容器上调用成员函数等都可能导致std::logic_error
异常。
这个异常类也定义在头文件<stdexcept>
中,并且是std::exception
的直接派生类。
// bad_alloc example
#include <iostream> // std::cout
#include <new> // std::bad_alloc
int main () {
try
{
int* myarray= new int[10000];//分配失败隐式抛出异常(仅限部分异常)
}
catch (std::bad_alloc& ba)//捕获抛出异常类bad_alloc的对象
{
std::cerr << "bad_alloc caught: " << ba.what() << '\n';
}
return 0;
}