C++
C++用于QT、opencv图像处理和游戏开发
C与C++的区别
- C++是C语言的超集,C++完全兼容C语言
- C语言是面向过程的编程思想,C++是面向对象和泛型的编程思想。面向对象(OOP)支持的新内容:对象、类、封装、数据隐藏、多态和继承等。
OPP的特性:抽象、封装和数据隐藏、多态、继承、代码的可重用性。
1. C++基础
1.1 名称空间
由于当项目的增大,名称相互冲突的可能性也将增加,所以C++标准提供了名称空间工具,以便更好地控制名称地作用域。例如,下面地代码使用新的关键字namespace创建了一个名称空间Jill。
namespace Jill{
double bucket(double n){...}
double fetch;
int pal;
struct Hill{...};
}
名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的。
C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。当使用<iostream.h>时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;当使用的时候,该头文件没有定义全局命名空间,必须使用namespace std。
使用名称空间的名称:
(1)使用名称空间名限定该名称(名称空间名::变量名)
namespace Jill{
double bucket(double n){...}
double fetch;
void menu();
int pal;
struct Hill{...};
}
int main()
{
cin >> Jill::fetch;
Jill::menu();
}
(2)using声明(using namespace 名称空间名)
#include <iostream.h>
using namespace Jill; //全局声明切换名称空间
int main()
{
using namespace Jill; //mian函数内声明切换名称空间
...
}
1.2 输入cin和输出cout
输入时,cin使用>>运算符从输入流中抽取字符到右侧到变量中。
输出时,cout使用<<运算符将字符串插入到输出流中,可以连续使用拼接成一句话。
endl确保程序在运行前刷新输出,“\n”却不能提供。
示例:
cin >> carrots;
cout << "Now you have" << carrots << " carrots." << endl; //endl即换行
1.3 在C++中使用C库函数
在C语言中,一个名称只对应,C语言编译器可能将spiff这样的函数名翻译为_spiff。但在C++中,同一个名称可能对应多个函数,C++编译器为重载函数会生成不同的名称,例如,可能将spiff(int)转换为_spiff_i,将spiff(double, double)转换为_spiff_d_d。
如果要在C++程序中使用C库预编译的函数,就会找不到对应的函数,解决方法如下例,在头文件中如下声明:
extern "C"
{
//使用C编译的函数
void spiff(int);
...
}
extern "C++"
{
//使用C++编译的函数
void menu(int);
...
}
1.4 如何判断一段程序是由C 编译程序还是由C++编译程序编译的
#ifdef __cplusplus
printf("c++\n");
#else
printf("c\n");
#endif
1.5 NULL、0还是nullptr
在C中,空指针可以用0或NULL来表示,通常使用NULL来指出其为指针。而在C++中,更喜欢使用传统的0,而不是等价的NULL。在C++11中提供了更好的nullptr供使用。
1.6 小记
#pragma pack(2) //使用两字节对齐
#pragma once //声明一次 相当于使用ifnodef
2. 函数
2.1 引用变量
C++新增了一种符合类型——引用变量。引用是已定义的变量的别名(另一个名称)。引用变量的主要用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始空间数据,而不是复制原数据。C++使用&符号来声明引用。
实例:
(1)创建使用引用变量
int rats;
int & rodents = rats;
(2)将引用用作函数参数
void swap(int & a, int & b) //交换函数
{
int temp;
temp = a;
a = b;
b = temp;
}
int main()
{
int x = 100;
int y = 200;
swap(x, y);
...
}
注意:不要返回指向局部变量或临时对象的引用,因为函数执行完,这些将被释放。
引用和指针的区别:
<1> 引用是起别名,指针是保存地址
<2> 引用不会分配空间,指针需要额外的空间
<3> 定义引用时就必须初始化,指针不是必须的
<4> 引用可以改变引用对象的值,但不能改变引用的对象,指针可以改变指针指向
(3)如果将引用作为函数参数的值传递(可减少使用传值导致的调用复制构造函数),而不是修改原始值,则应尽可能使用const,原因如下:
- 避免无意中修改的编程错误。
- 使函数可以处理const实参和非const实参,而非只能接受非const数据。
- 使用const引用使函数能正确生成并使用临时变量
2.2 默认参数
默认参数指的是当函数调用中省略了实参时自动使用的一个值。
示例:
int groucho(int k = 1, int m = 2, int n = 3);
对于带参数列表的函数,必须从右向左添加默认值。而使用实参必须按从左到右的顺序依此赋给形参,而不能跳过任何参数。因此,如下的方式是错误的:
int groucho(int k = 1, int m = 2, int n);
beeps = groucho(3, ,8);
注意:默认参数只能出现一次,不可同时出现在声明和定义处。
2.3 函数重载
默认参数能够使用不同数目的参数调用同一个函数,而函数重载(函数多态)能够使用多个同名的函数。
函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。C++允许定义名称相同的函数,条件是它们的特征标不同。例如可以定义一组原型如下的print()函数:
void print(const char * str, int width);
void print(double d, int width);
void print(long l, int width);
void print(int i, int width);
void print(const char *str);
printf(1999.0, 10);
在调用时,会自动匹配特征标相同的函数。没有匹配的原型并不会自动停止使用某个函数,因为C++将使用标准类型转换强制匹配。
注意:如果是重新构建了一个使用引用的函数,这将并不能发生重载,因为对从本质上来说,他们都是同一种数据类型的传参。
3. 类
类是用户定义的一种数据类型。类之于对象就像类型之于变量。也就是说,类定义描述的是数据类型的全部属性(包括可使用它执行的操作),而对象则是根据这些描述创建的实体。
3.1 实现一个类
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。C++提供了三种访问控制关键字:private(私有)、public(公有)和protect(受保护)。类声明时,不使用访问控制关键字则默认使用private。
- 类方法定义: 描述如何实现类成员函数
简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。通常,数据成员被放在私有部分中,成员函数被放在公有部分中。
(1)典型的类声明的格式如下:
class World
{
float mass; //private
char name[20]; //private
public:
void tellall(void);
...
};
(2)创建类对象
World kate, joe;
(3)使用类成员
kate.tellall();
joe.tellall();
3.2 this指针
在成员函数的实现过程中,如有访问到类自己的数据成员的时候建议使用this指针,避免因为形参名字和数据成员名字相同造成的混乱。
使用方法如下:
类对象访问成员:
对象名.成员名
类指针访问成员:
指针名->成员名
注意:每个成员函数(包括构造和析构函数)都有一个隐藏的this指针。this指针指向调用对象,保存的是对象的地址。如果要引用整个调用对象,则可以使用表达式*this(将解除引用运算符*用于指针,将得到指针指向的值)。在函数的括号后使用const限定符将使this限定为const,这意味着不能使用this修改对象的值。
3.3 构造函数
3.3.1 声明和定义构造函数
构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数。另外,构造函数没有声明类型(无返回值)。通常,构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。
使用方法如下:
//声明
class phone
{
public:
phone(string brand = "mi", int price = 2000, string color = "blue");
...
}
//定义
phone::phone(string brand, int price, string color)
{
cout << "phone(brand, price, color)" << endl;
this->brand = brand;
this->price = price;
this->color = "red";
}
3.3.2 默认构造函数
如果创建对象时没有进行显式地初始化,则将调用默认构造函数。如果程序中当且仅当没有定义任何构造函数时,则编译器才会提供默认构造函数,否则,必须自己提供默认构造函数。默认构造函数可以没有任何参数,如果有,则必须给所有参数提供默认值。所以,默认构造函数只能存在一个,无参或全参提供默认值。
phone first(); //error
phone second;
注意以上两种调用方式,第一种是指,second()是一个返回phone对象的函数。第二种才是调用默认构造函数,不带圆括号。
3.3.3 拷贝构造函数(复制构造函数)
拷贝构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程。类的拷贝构造函数原型通常如下:
Class_name(const Class_name &);
StringBad(const StringBad &);
3.3.3.1 何时调用复制构造函数
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。最常见的情况就是将新对象显式地初始化为现有地对象。例如下面4中种声明都将调用复制构造函数:
StringBad ditto(motto);
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad * pStringBad = new StringBad(motoo);
3.3.3.2 浅拷贝和深拷贝
警告:如果类中包含了使用new初始化地指针成员,应定义一个复制构造函数,以复制指向地数据,而不是指针,这被定义为深度复制。复制地另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。否则这将造成析构函数释放空间时,对两个指针指向的同一片空间释放两次出现错误。
默认的复制构造函数逐个复制只非静态成员的值,所以如果类中有静态成员则应提供一个显式的复制构造函数。
3.3.3.3 列表初始化
通常构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。如下例:
Bozo(const char * fname, const char * lname); //构造函数
//初始化
Bozo bozetta = bozo("Bozetta", "Biggens");
Bozo fufu("FuFu", "O'Dweeb");
Bozo *pc = new Bozo("Popo", "Le Peu");
//C++11,列表初始化
Bozo bozetta = {"Bozetta", "Biggens"};
Bozo fufu{"FuFu", "O'Dweeb"};
Bozo *pc = new Bozo{"Popo", "Le Peu"};
如果构造函数只有一个参数,则将对象初始化为一个与参数的类型相同的值时,构造函数将被调用。
3.4 析构函数
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象声明周期结束时,程序将自动调用一个特殊的成员函数,该函数即析构函数。
3.4.1 声明和定义析构函数
和构造函数一样,析构函数也可以没有返回值和声明类型,析构函数的名称和类名相同。与构造函数不同的是,析构函数没有参数,且函数名前需要加上~。
//声明
class phone
{
public:
~phone();
...
}
//定义
phone::~phone()
{
cout<< this->brand << " finish!" << endl;
}
3.4.2 使用new的构造函数对应的析构函数
示例:
String::String() //构造函数
{
str = new char[1];
...
}
String::~String() //析构函数
{
delete [] str;
...
}
String* username = new String(); //分配空间
delete String; //释放空间
应注意:
- 先创建的后析构
- 如果构造函数中使用new来分配空间,则析构函数将使用delete来释放这些内存,new对应delete,new[]对应delete[]。
3.5 类的大小和空类对象
(1)每个所创建的新对象都有自己的存储空间,用于存储其内部变量和类成员,但同一个类所有对象共享同一组类方法(函数),即每种方法只有一个副本,不同的对象调用时将执行同一个代码块,只是这些代码用于不同的数据。
(2)定义类时,并不分配空间,只有定义对象系统才会自动分配空间(栈区),空间大小只取决于数据成员占内存的大小,且字节对齐。
(3)C++标准不允许一个对象(当然包括类对象)的大小为0,保证可以访问,不同的对象不能具有相同的地址。所以在VS中C++空类的大小为1Byte,不同的编译器可能不同。
空类中默认包含的成员函数:6个
默认构造函数、析构函数、拷贝构造函数、赋值运算符函数、&运算符函数、&运算符函数 const
3.6 类的自动转换和强制类型转换
下面的构造函数用于将double类型的值转换为Stonewt类型:
Stonewt::Stonewt(double lbs){...}
这意味着如下的代码是可行的:
Stonewt myCat;
myCat = 19.6;
程序将使用构造函数Stonewt(double)来创建一个临时的Stonewt对象,并将19.6作为初始化值,然后采用逐成员赋值方式将临时对象的内容复制到myCat中。这一过程称为隐式转换。
只有接受一个参数的构造函数才能作为转换函数。
Stonewt(double lbs); //可转
Stonewt(int stn, double lbs); //不可转
Stonewt(int stn, double lbs = 0); //可转
3.6.1 创建一个转换函数
需要如下的函数形式:
operator typeName();
注意:
- 转换函数必须是类方法;
- 转换函数不能指定返回类型;
- 转换函数不能含参;
3.6.2 explicit关键字
前述提到的转换函数在C++中可使用explicit关闭。也就说,可如此声明构造函数时:
explicit Stonewt(double lbs);
虽然这将关闭隐式转换,但仍可如下进行显式强制类型转换:
Stonewt myCat;
myCat = Stonewt(19.6);
mycat = (Stonewt) 19.6;
3.7 类的赋值运算符
如果将一个对象赋给同一个类的另一个对象,编译器将自动为这个类提供一个赋值运算符。这个运算符的默认版本将采用成员赋值,即将原对象的相应成员赋给目标对象的每一个成员。
如果将派生对象赋给基类对象,编译器将使用基类运算符来处理派生对象中基类部分的赋值。建议使用dynamic_cast动态转换,不建议static_cast静态转换。
如果将基类对象赋给派生对象,则不一定成功。static_cast静态转换访问可能越界,危险。dynamic_cast动态转换安全,但不建议使用。总之不建议这样赋值。
应注意:如果将一个类对象赋值给另一个正在初始化的对象时,使用的是复制构造函数。
4. 说明符和限定符
4.1 静态限定
4.1.1 静态变量
所有的静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置了0,这被称为零初始化。
关键字static有两种用法,但含义不同:用于局部声明,以指出变量使无链接性的静态变量时,static表示的是存储持续性;而用于代码块外的声明时,static表示内部链接性,而变量已经是静态持续性了。
4.1.2 静态变量的初始化
除默认的零初始化外,还可对静态变量进行常量表达式初始化和动态初始化。对于标量类型,零将被强制转换为合适的类型。
零初始化和常量表达式初始化被统称为静态初始化,这意味着编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。
首先,所有静态变量都被零初始化,而不管是否显式地初始化了。接下来,如果使用常量表达式初始化了变量,且编译器仅根据文件内容(包含被包括的头文件)就可计算表达式,编译器将执行常量表达式初始化。
4.1.3 静态类成员
静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态变量副本。也就是说,类的所有成员共享同一个静态成员。所有静态类成员可以记录所有类对象共有的相同值的私有数据。
示例:
//声明
class String
{
private:
static int num;
...
}
//初始化
int String::num = 0;
注意:
- 不能在类声明中初始化静态成员变量。这是因为声明仅描述了如何分配内存,但并不分配内存。所以可以在类声明之外使用单独的语句进行初始化。如果静态成员是const整数类型或枚举,则可在类中初始化。
- 对于静态类成员不仅可以使用对象调用进行初始化,也可以在声明外使用单独的语句进行初始化,这是因为静态类成员是单独存储的,并不属于任何对象,属于整个类。
- 初始化语句指出了类型,并使用了作用域运算符,但没有使用关键字static。
- 若静态变量是私有的,则在类外需要通过成员函数使用this指针间接访问,不可直接访问。
- 初始化是在方法文件中,而不是在类声明文件中,这是因为类声明位于头文件中,程序可能多次引用此头文件,这出现多次初始化语句,引发错误。
访问方式:
类名::静态数据成员
cout<<Student::sno<<endl;
对象名.静态数据成员
Student stu1;
cout<<stu1.sno<<endl;
指针名->静态数据成员
Student *ps=new Student;
cout<<ps->sno<<endl;
4.1.4 静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static)。
静态成员函数的特点:
- 不能通过对象调用静态成员函数,所以甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。
- 由于静态成员函数不和任何对象关联,所以只能使用静态数据成员。
使用方法如下例:
//向String类添加一个名为HowMany()的静态成员函数
static int HowMany(){return num_strings;}
//通过此函数间接调用静态数据成员static int num_strings
int count = String::HowMany();
4.2 const限定符
4.2.1 const修饰变量
使变量成为常量,将不允许修改。
创建常量的通用格式如下:
//在声明中进行初始化
const type name = value;
//以下初始化的方式是错误的
const int num;
toes = 10;
const char * const months[12] = {"Jan", "Feb",...};
在上式中,第一个const防止字符串被修改,第二个const确保数组中每个指针始终指向最初的字符串,指向不被修改。
4.2.2 const修饰数据成员
使数据成员成为常量,将不允许修改。只能使用如下的初始化方法,称为成员初始化列表:
//Classy是一个类,而mem1、mem2、mem3都是这个类的数据成员,分别初始化了n、0、n*m + 2。
Classy::Classy(int n, int m) :mem1(n), mem2(0), mem3(n*m + 2)
{
//...
}
注意:
- 这种格式只能用于构造函数。
- 必须用这种格式来初始化非静态const数据成员。
- 必须用这种格式来初始化引用数据成员。
- 成员初始化列表也可用于常规初始化。如:
int games= 125; 可替换为 int games(125);
4.2.3 const修饰成员函数
const修饰成员函数可以保证函数不会修改调用对象,被称为常函数。使用方法如下:
void show() const; //声明
void stock::show() const{} //定义
注意:
- const修饰的成员函数也不能使用this指针,因为this指针也是const不可更改。
- 在const修饰的成员函数中不能调用非const修饰的数据成员。
- 在非const修饰的成员函数中可以调用const修饰的数据成员。
4.3 mutable(不建议使用)
mutable可以使const结构(或类)变量的某个成员被修改,例如:
struct data
{
char name[30];
mutable int accesses;
...
};
const data veep = {"kkk" , 0, ...};
strcpy(veep.name, "qqq"); //不能修改
veep.accesses++; //可以修改
5. 动态分配内存空间
(1)在C++中,使用new和delete运算符来完成分配和释放空间,如下例:
int * ps = new int; //1个int
float *p_free = new float [20] //20个float
(2)如要初始化可在类型名后面加上初始值,并用括号括起来,如下例:
int * pi = new int (6); //初始化6
double * pd = new double (9.99); //初始化9.99
int * parr = new int [10](); //初始化'\0'
struct where {double x; double y; double z};
where * one = new where {2.5, 5.3, 7.2}; //初始化结构体,需支持C++11
int * ar = new int [4] {2, 4, 6, 8}; //初始化数组,需支持C++11
(3)当new失败时,new将会返回空指针,但现在将引发异常std::bad_alloc。
释放内存时,如下例:
delete [] pd;
(4)new和malloc的区别:
- new是运算符,malloc是C标准库函数
- new无需强制类型转换,而malloc需要
- 初始化和释放空间的用法不同
- 对于类来说,使用new会自动调用构造函数,delete释放对象会自动调用析构函数。
6. 友元(不建议使用)
友元即是获得相同的访问权限,使其可以访问私有类对象。友元有3种:
- 友元函数
- 友元类
- 友元成员函数
6.1 友元函数
友元函数即是通过让函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限,使函数可以访问类的私有函数。
创建友元函数的步骤如下:
第一步:将其原型放在类声明中,加上friend:
friend CNum updatesocre(CNum& m, double n);
注意:updatesocre()不是成员函数,不需要成员运算符,可以直接调用。
第二步:编写函数定义。定义如下:
CNum updatesocre(CNum& m, double n)
{
m.score = n;
}
6.2 友元类
使一个类成为另一个类的友元:
友元类有多个成员函数试图访问被友元的类的私有数据成员。
创建友元类的步骤如下:
第一步:将其原型放在类声明中,加上friend:
class CDes
{
public:
friend class CNum; //声明CDes的友元类CNum
...
private:
double socore;
...
}
第二步:编写类定义。定义如下:
class CNum:
{
public:
void updatesocre(CDes& des, double value);
...
}
void CNum::updatesocre(CDes& des, double value)
{
des.socre = value;
}
通过上述代码即可使CNum中可编写多个成员函数去访问CDes的数据成员。
6.3 友元成员函数
实际上,我们很多时候并不需要一整个类去作友元,真正有需要的是某个类中的成员函数成为另一个类的友元,下面就介绍这种方法:
class CNum;
class CDes
{
public:
friend void CNum::updatesocre(CDes& des, double value); //声明CDes的友元类CNum
private:
double socore;
...
}
注意:要使编译器能正常编译需要在类中声明友元前加先去声明友元的类,否则其将不认识CNum。这种办法被称为前向声明(forward declaration)。
class CNum:
{
public:
void updatesocre(CDes& des, double value);
}
void updatesocre(CDes& des, double value)
{
des.socre = value;
}
7. 运算符重载
C++允许将运算符重载扩展到用户自定义的类型,例如,允许使用+将两个对象相加,编译器将根据操作数的数目和类型决定使用哪种加法定义。重载运算符可使代码看起来更自然。
要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数格式如下:
operatorop(argument-list)
//operator+()重载+运算符;operator*()重载*运算符
7.1 以下使用++运算符运算复数举例:
//友元函数重载前置++运算符
CComplex& operator++(CComplex &a) //引用传参不进行复制构造,速度更快,占用更少
{
a.real++; //数据成员实部值
a.img++; //数据成员虚部值
return a;
}
//数据成员函数重载前置++运算符
CComplex& operator++()
{
this->real++; //数据成员实部值
this->img++; //数据成员虚部值
return *this; //返回对象本身
}
//友元函数重载后置++运算符
CComplex& operator++(CComplex &a, int) //多一个参数int可使系统识别为后置++
{
CComplex temp = a;
a.real++; //数据成员实部值
a.img++; //数据成员虚部值
return res; //返回对象本身
}
//数据成员函数重载后置++运算符
CComplex& operator++(int) //多一个参数int可使系统识别为后置++
{
CComplex temp = *this;
this->real++; //数据成员实部值
this->img++; //数据成员虚部值
return res; //返回对象本身
}
CComplex a(1, 2);
//隐式调用,友元函数和数据成员函数只能存在其一
a++;
//显式调用
a = operator++(a, 0);
左移运算符重载:
void operator<<(ostream &os, CComplex& a) //使用友元函数
{
os << a.real << "+" << a.img << "i" <<endl;
}
cout << a; //调用,相当于os = cout,a = a
对于友元重载运算符函数来说,运算符左边的操作数对应函数的第一个参数,右边的操作数对应函数的第二个参数。
由于成员函数其中一个操作数通过this指针隐式传递,所以成员函数所需参数比友元函数少一个。
7.2 重载限制
大多数C++运算符都可如此重载,重载方便但也有限制:
(1)隐式调用时,此方式友元函数和数据成员函数只能存在其一。
(2)重载不能改变运算符的操作数,重载后的运算符必须至少有一个操作数是用户定义的类型,为了防止用户为标准类型重载运算符,如将+重载为-。
(3)使用运算符不能违反原先的句法规则。例如,不能将求模(%)重载成操作数。
(4)不能修改运算符的优先级,所以重载后的拥有和原先相同的优先级。
(5)运算符重载不允许有默认值。
(6)下列运算符只能通过成员函数进行重载:
- =:赋值运算符
- ():函数调用运算符
- []:下标运算符
- ->:指针访问类成员的运算符
(6)不可重载的运算符: - sizeof sizeof运算符
- . 成员运算符
- .* 成员指针运算符
- :: 作用域解析运算符
- ?: 条件运算符
- typeid 一个RTTI运算符
- const_cast 强制类型转换运算符
- dynamic_cast 强制类型转换运算符
- reinterpret_cast 强制类型转换运算符
- static_cast 强制类型转换运算符
8. 模板——泛型编程的体现
模板就是使数据类型参数化来创建类或函数。
8.1 函数模板
8.1.1 函数模板定义
如下例,定义一个交换函数模板:
template <typename T> //typename也可使用class,这是C++98的习惯,不过如今不考虑向下兼容时,应尽量使用typename
void swap(T &a, T &b) //T即类型名,符合命名规则即可,通常使用T命名
{
T temp;
temp = a;
a = b;
b = temp;
}
8.1.2 函数模板重载
和常规重载一样,重载的函数模板特征标必须不同。以下定义一个与上例特征标不同的函数模板:
template <typename T, typename T1>
void swap(T &a, T1 &b) //T和T1可同类型,也可不同类型
{
T temp;
temp = a;
a = b;
b = temp;
}
8.1.3 实例化
实例化方式如下:
//通过声明实例化
template void swap<int>(int, int); //使用<>指示类型,并在声明前加上关键字template
//通过调用实例化
int a = 5, b = 6;
swap<int>(a, b);
注意:函数模板并不占用内存,只有在实例化时,生成对应的函数,此时函数会占用内存,并且相同类型的函数只生成一次,下次将直接调用。
8.2 类模板
8.2.1 定义类模板
以下以定义一个链表类模板为例:
template<class T>
//节点类
class CNode
{
public:
CNode()
{
this->pnext = nullptr;
}
T data; 数据域
CNode <T>* pnext; //指针域
private:
}
//链表类
template<class T>
class CList
{
public:
CList()
{
this->head = new CNode<T>;
this->count = 0;
}
~CList()
{
delete this->head;
}
void push_back(T data)
{
//定义一个临时的指针变量,指向头节点
CNode<T>* ptemp = this->head;
//开辟新节点
CNode<T>* newNode = new CNode<T>;
newNode->data = data;
//找到尾节点即指针域为空
while (ptemp->pnext!=nullptr)
{
ptemp = ptemp->pnext;
}
// 找到后连接新节点
ptemp->pnext = newNode;
this->count++;
}
int getCount();
void printInfo()
{
//定义一个临时的指针变量,指向头节点
CNode<T>* ptemp = this->head;
//找到尾巴节点===>指针域为空
while (ptemp->pnext != nullptr)
{
ptemp = ptemp->pnext;
(ptemp->data)->print(); //不同的data域会导致不同的调用打印函数方式,非指针就是'.',所以此并不通用
}
}
private:
CNode<T>* head; //链表头节点
int count;
}
template<class T>
int CList<T>::getCount()
{
return this->count;
}
class CComplex
{
public:
CComplex(int real = 0, int img = 0)
{
this->real = real;
this->img = img;
cout << "CComplex(int, int)" << endl;
}
void print()
{
cout << this->real << "+" << this->img << "j" << endl;
}
private:
int real;
int img;
};
int main()
{
CList<CComplex*> list_int;
list_int.push_back(new CComplex(1,2));
list_int.push_back(new CComplex(2,3));
cout << list_int.getCount() << endl;
list_int.printInfo();
return 0;
}
注意:
- 类模板中的每一个函数都是函数模板
- 类模板的实现不支持分文件
9. STL标准模板库
STL中的六大组件:算法、容器、迭代器、函数对象、分配器、适配器。
算法是完成特定任务的处方。
容器是一与数组类似的单元,可以存储若干个类型相同的值。
迭代器能够遍历容器的对象,是广义指针。
函数指针是类似于函数的对象,可以是类对象或函数指针(函数名也是函数指针)。
9.1 vector向量容器
vector类似于string类,也是一种动态数组。
特点:
- 地址连续,可索引访问,效率高
- 插入删除需进行大量搬移,效率相对低
- 数据满时,会在内存中寻找另外一片更大(1.5-2倍)的连续空间替代原先的空间
9.2 list模板类
list模板类表示双向链表。数据量较大时,需要多次插入或删除时使用。
特点:
- list强调的是元素的快速插入和删除
- list不支持索引访问,只能使用迭代器访问
- 多出了指针域,地址不连续,但相对需要更大的空间
9.3 map
关联容器是对容器的一种改进,其将值和键关联在一起,并使用键来查找值。
关联容器的优点在于对元素的快速访问。
在map中,值和键的类型不同,键是唯一的,且和值一一对应。根据key自动排序:按从小到大的顺序进行排序。
使用map做一个英汉词典:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include<cstring>
#include <fstream>
#include <map>
using namespace std;
int main()
{
ifstream fin;
//打开文件
fin.open("1.txt", ios_base::in);
if(!fin.is_open())
{
exit(EXIT_FAILURE);
}
// 创建string string的map
map <string, string> trans;
string strsrc;
char en[100];
char cn[100];
while (getline(fin, strsrc, '\n'))
{
//cout << strsrc << endl;
//strsrc.c_str()将string转换为字符数组
//%[^ ]表示读取字符直到遇到空格为止,%[^\n]表示读取字符直到遇到回车为止
int num = sscanf(strsrc.c_str(), "%[^ ] %[^\n]", en, cn);
trans.insert(pair<string, string>(en, cn));
}
fin.close();
//查询
cout << "请输入要查询的单词:" << endl;
string word;
cin >> word;
//创建迭代器
map<string, string>::iterator temp;
//迭代器指向查询到的位置
temp = trans.find(word);
if (temp != trans.end())
{
cout << temp->second << endl;
}
else
{
cout << "未查到" << endl;
}
return 0;
}
10. 类继承
从已有的类派生出新的类,派生类(也称子类)继承了原有类(称为基类,也称父类)的特征和方法,称为类继承。
也就是说,如果一个类是另外一个类的特殊版本的情况,那就可以使用继承。
10.1 类的权限和继承权限
不能被继承的内容:构造函数、析构函数和赋值运算符(因为它包含一个类型为其所属类的形参)。友元函数非类成员,因此不能被继承。
继承的方式有3种: public、protected、private。
类的三种权限:
(1)public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用。
(2)private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以调用。
(3)protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。
继承方式:
公有继承:继承自父类的成员保持不变。
私有继承:继承自父类的成员全部变为私有成员。
保护继承:继承自父类的公有成员变为保护成员,其余不变。
所以通常使用public的方式继承,数据成员放在protected区。
10.2 派生一个类
class RatedPlayer : public TableTennisPlayer
{
...
};
冒号指出RatedPlayer类的基类是TableTennisPlayer类。上述声明表明TableTennisPlayer是一个公有基类,这被称为公有派生。如不使用public将默认为私有派生。
派生类具有以下特征:
- 派生类对象包含基类的数据成员,可以使用基类的方法。
- 基类的公有成员将称为派生类的公有成员。
- 基类的私有部分也称为派生类的一部分,但只能通过基类的公有和保护方法访问。
10.3 派生类的构造函数
派生类需要自己的构造函数,根据需要添加额外的数据成员和成员函数。
注意:
创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数复制初始化继承的数据成员,派生类构造函数主要用于初始化新增的数据成员。可以使用初始化列表语法指名要使用的基类构造函数,否则将使用默认的基类构造函数。
派生类对象过期时,程序将先调用派生类析构函数,在调用基类析构函数,这也符合析构函数的原则——后构建的先析构。
指定初始化列表的方式如下例:
derived::derived(type1 x, type2 y, type3 z) : base(x, y), z(z)
{
...
}
其中derived是派生类,base是基类,x和y是基类构造函数使用的变量,z是派生类构造函数使用的变量。
在子类中可以以如下方式调用父类的成员函数basefunc1()
derived dderived; //创建子类
dderived.basefunc1();
10.4 多层继承
多层继承就是从派生类中再次产生派生类的形式,多层派生一定是从最上层的构造函数执行,从最后执行的构造函数开始析构。经典模型为基类——派生类1——派生类2。
10.5 多重继承——菱形继承
现有此例,Worker派生出了Singer和Waiter,Singer和Waiter一起派生出了SingingWaiter。
10.5.1 多余的最上层基类
由于Singer和Waiter一起派生出了SingingWaiter,所以Singer和Waiter都继承了一个Worker组件,也就是说在创建SingingWaiter,会导致创建两个Worker,在使用Worker就会出现二义性。
解决此问题的办法就是虚基类。
10.5.2 虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如,通过在类声明中使用关键字virtual,可使Worker被用作Singer和Waiter的虚基类(virtual和public的次序无关):
class Singer : virtual public Worker{...};
class Waiter : public virtual Worker{...};
然后,可以将SingingWaiter类定义为:
class SingingWaiter : public Singer, public Waiter{...};
注:
- 为了实现虚拟继承,派生类对象的大小会增加4Byte。这增加的4个字节,是因为当虚拟继承时,无论是单虚继承还是多虚继承,派生类需要有一个虚基类表来记录虚继承关系,所以此时子类需要多一个虚基类表指针,而且只需要一个即可。每多一个虚基类,虚基类表会多加4个字节。
- 如果从基类中继承来的方法是实现的效果满足不了派生类的需求,那么就可以在派生类中定义和派生类同名的函数。
10.5 类与类的关系
类与类的经典关系有两种,is-a关系就是继承关系(banana is a fruit),has-a关系就是包含关系(fruit has a banana)。
11. 多态
方法的行为取决于调用该方法的对象,这种行为就称为多态。
11.1 多态的分类
C++中多态分为静态多态和动态多态两种。
静态多态(静态联编/静态绑定):程序在编译时就能确定是哪一个函数体,主要通过函数重载和运算符重载来体现。
动态多态(动态联编/动态绑定):程序在运行时才能确定执行的函数体,主要通过继承和虚函数来实现。
11.2 动态多态实现
现有如下例:
#include <iostream>
using namespace std;
class Shape
{
public:
Shape()
{
cout << "Shape()" << endl;
this->l = 0;
this->s = 0;
}
double getL() //应转换虚函数
{
cout << "Shape获取L的方法" << endl;
return this->l;
}
~Shape()
{
cout << "~Shape()" << endl;
}
protected:
double l;
double s;
};
class Rect :public Shape
{
public:
Rect(int w = 3, int h = 5)
{
cout << "Rect(int,int)" << endl;
this->w = w;
this->h = h;
}
double getL()
{
cout << "Tect获取L的方法" << endl;
this->l = (this->w + this->h) * 2;
return this->l;
}
~Rect() { cout << "~Rect()" << endl; }
private:
int w, h;
};
class Circle :public Shape
{
public:
Circle(int r = 3)
{
cout << "Circle(int)" << endl;
this->r = r;
}
double getL()
{
cout << "Circle获取L的方法" << endl;
this->l = 2 * this->r * 3.1415;
return this->l;
}
private:
int r;
};
void calS_L(Shape& shape) // Shape * shape = ▭ 把子类对象的地址赋值给了父类类型的指针变量
{
cout << shape.getL() << endl;
}
int main()
{
Rect rect(5, 8);
calS_L(rect); // g++编译器会根据函数名、参数类型以及参数个数对函数名进行重命名
Circle circle(8);
calS_L(circle);
return 0;
}
运行结果如下:
Shape()
Rect(int,int)
Shape获取L的方法
0
Shape()
Circle(int)
Shape获取L的方法
0
~Shape()
~Rect()
~Shape()
造成这样的原因就是通过父类的指针尝试调用重写后子类的函数,但只能调用到从父类继承的函数。
解决的办法就是将基类对应的函数加上virtual关键字,将其变为虚函数:
virtual double getL(){...}
11.2.1 指针和引用类型的兼容
通常,C++不允许将一种类型的地址赋值给另一种类型的指针,也不允许一种类型的引用指向另一种类型。
但派生类的引用或指针可以转换为基类的引用或指针,不需要显式类型转换,这被称为向上强制转换。
相反的,将基类的引用或指针转换为派生类的引用或指针被称为向下强制转换。但必须使用显式类型转换,这是由于is-a的关系通常是不可逆的。
11.2.2 虚函数的实现原理
通常,编译器处理虚函数的方法是:给每一个对象添加一个隐藏成员,这个隐藏成员就是一个指向函数指针数组的指针,这个数组就被称为虚函数表。虚函数表中储存了为类对象声明的所有虚函数的地址。
例如:基类对象包含了一个指向虚函数表的指针,派生类对象包含了一个指向另一个虚函数表的指针。如果派生类对虚函数没有进行重写,那派生类将复制基类的该虚函数地址。如果派生类对虚函数进行了重写,其虚函数表会将重写后新的虚函数地址覆盖原来地址。如果派生类定义了新的虚函数,其地址就将添加进虚函数表中。
当调用虚函数时,程序将通过虚函数表的指针,查找虚函数表中对应函数的地址并执行。
所以在创建类对象时,会在内存中额外分配一个4byte(32OS)或8byte(64OS)的虚函数表的指针和一个虚函数表。
11.2.3 动态多态的实现条件
多态:
(1)一个父类,多个子类。
(2)父类中要使用虚函数(可使在派生出的类中该函数也是虚的),并在子类中重写。
动态:
(3)只有通过父类类型的指针或引用去操作子类对象,调用虚函数的时候才会去触发动态绑定,调用虚函数表。如果使用按值传递,将导致调用子类时,不会发生类型转换,仍会使用父类方法,只使用子类中父类的那部分对象。如void calS_L(Shape& shape)去掉&将导致即是实参是子类,仍使用shape的方法。
总之,有继承也有虚函数也不一定触发多态。
11.2.4 虚函数的注意事项
(1)构造函数不能是虚函数,这是因为创建一个派生类时,会调用派生类的构造函数,然后为此将使用基类的一个构造函数,这个机制不同于继承。
(2)通常应给基类提供一个虚析构函数,即是它并不需要析构函数。原因如下例:
...
Shape* shape = new Rect(5, 6);
delete shape;
...
该程序将会导致在delete时,只使用了父类的析构函数,释放父类的那部分内存,并不会执行子类的析构函数,导致内存泄露。
11.3 抽象类
一个类中只要有纯虚函数,那么这个类就是抽象类。抽象类只能被继承,不能创建对象,且子类如不重写纯虚函数,也为抽象类。
纯虚函数的格式:
virtual 函数类型 函数名(参数列表) = 0;
11.4 接口类——抽象类的应用
接口类用来描述多种能力或者表达一些协议,只能被继承,用来确保所有的派生类至少都包含接口类的方法。
接口类中只有成员函数,没有数据成员,并且所有的成员函数都是纯虚函数。
11.5 重载、重写、重定义的区别
重载:在同一个作用域内,函数功能相似,函数名相同,参数不同(参数类型、参数个数、参数类型的顺序)且与返回值无关的一组函数互为重载。重载是编译时多态,即静态多态。
重写(覆盖):在继承关系中,子类重写了父类的虚函数。函数名相同,参数相同,与返回值无关。也就是说,重写只能重写对应的那一个父类函数。重写是运行时多态,即动态多态。
重定义(隐藏):在继承关系中,子类定义了和父类同名的函数,且只需函数名相同。所以子类可以隐藏父类所有的(重载)同名函数。
12. 拓展讨论
12.1 关键字
(1)Explicit关键字
修饰构造函数,防止构造函数单参数隐式类型转换。
(2)Final关键字
- final修饰的类是不能被继承的。
- final修饰的是虚函数,在子类中不允许重写。
(3)C++内联函数————Inline关键字
由于函数执行,需要跳到函数所在内存(堆区)地址执行,执行完成后再跳回去继续执行原有代码(主函数——栈区),造成频繁的出入栈,占大量的内存开销,影响执行效率。
内联函数就是使用函数体替换函数语句直接去执行。
使用方式:在函数声明前和函数定义前加上关键词inline。 - 宏函数:本质是宏,只是可以实现和函数一样的效果。特点是代码短小、逻辑简单、直接进行文本替换。
- 宏函数与内联函数的区别:
<1>本质:一个是宏,一个是函数。
<2>特点:宏函数是代码短小、逻辑简单的代码块;内联函数就是使用函数体替换函数语句直接去执行
<3>用法:宏函数由于是直接进行文本原样替换,每一部分都要加括号;
传参上:宏函数不进行类型检查,默认全部按text处理
<4> 发生阶段:宏函数在预处理阶段处理;内联函数在编译阶段处理。
12.2 类型转换
12.3 异常处理
C++中提供了一种新的异常处理机制:异常捕获
try
{
可能会出现异常的代码块
// 抛出异常
throw 异常;
}
catch(数据类型 形参) // 捕获异常
{
}
特点:
(1)异常是可以跨级处理的
(2)抛出的异常可以是各种类型,可以是基本数据类型,也可以是自定义的数据类型、类对象
(3)C++中有提供一整套的异常标准类
12.4 智能指针
共享智能指针 shared_ptr<类型>
独享智能指针 unique_ptr<类型>
弱型智能指针 weak_ptr<类型>
自动智能指针 auto_prt<类型>
12.5 单例设计模式
单例其实就是一个类只能实例化(创建)一个对象。
单例模式分类:
- 懒汉模式:只有在需要用到的时候才去准备
class CmyData
{
public:
static CmyData* getMydata(); //获取唯一的对象
private:
CmyData(); //构造函数
static CmyData* mydata; //保存唯一的对象
sqlite3* mydb; //保存打开的数据库对象
}
CmyData* CmyData::mydata = nullptr;
CmyData::CmyData() //构造函数
{
this->mydb = nullptr;
int res = sqlite3_open("./sqlite3/user.db", &this->mydb);
if (res != SQLITE_OK)
{
cout << "open database error" << endl;
}
else
{
cout << "open database ok" << endl;
}
}
CmyData* CmyData::getMydata()
{
if (CmyData::mydata == nullptr) //说明对象还没创建
{
CmyData::mydata = new CmyData; //构造函数只执行一次
}
return CmyData::mydata;
}
存在问题:多线程中,可能会出现多个线程同时去调用这个函数获取唯一的对象,有概率出现冲突,解决办法就是加锁。
- 饿汉模式:提前准备好
CmyData* CmyData::mydata = new CmyData;
不使用时,存在资源浪费。
// 抛出异常
throw 异常;
}
catch(数据类型 形参) // 捕获异常
{
}
特点:
(1)异常是可以跨级处理的
(2)抛出的异常可以是各种类型,可以是基本数据类型,也可以是自定义的数据类型、类对象
(3)C++中有提供一整套的异常标准类
### 12.4 智能指针
共享智能指针 shared_ptr<类型>
独享智能指针 unique_ptr<类型>
弱型智能指针 weak_ptr<类型>
自动智能指针 auto_prt<类型>
[外链图片转存中...(img-fQrIiJ7b-1688093404357)]
### 12.5 单例设计模式
单例其实就是一个类只能实例化(创建)一个对象。
单例模式分类:
- 懒汉模式:只有在需要用到的时候才去准备
```c++
class CmyData
{
public:
static CmyData* getMydata(); //获取唯一的对象
private:
CmyData(); //构造函数
static CmyData* mydata; //保存唯一的对象
sqlite3* mydb; //保存打开的数据库对象
}
CmyData* CmyData::mydata = nullptr;
CmyData::CmyData() //构造函数
{
this->mydb = nullptr;
int res = sqlite3_open("./sqlite3/user.db", &this->mydb);
if (res != SQLITE_OK)
{
cout << "open database error" << endl;
}
else
{
cout << "open database ok" << endl;
}
}
CmyData* CmyData::getMydata()
{
if (CmyData::mydata == nullptr) //说明对象还没创建
{
CmyData::mydata = new CmyData; //构造函数只执行一次
}
return CmyData::mydata;
}
存在问题:多线程中,可能会出现多个线程同时去调用这个函数获取唯一的对象,有概率出现冲突,解决办法就是加锁。
- 饿汉模式:提前准备好
CmyData* CmyData::mydata = new CmyData;
不使用时,存在资源浪费。