C++复习总结,仅供笔者复习使用,参考教材:
- 《零基础C++——从入门到精通》
- 《C++ Primer Plus》
本文主要内容为:C++ 结构体、面向对象和泛型编程
C++ 基本语法、输入输出、流程控制、字符串和数组部分 见 C++复习总结1,
C++ 指针、函数和STL容器 见 C++复习总结2.
目录
一. 结构体
结构体 struct 是 C 语言中的一种复合数据类型,由一些元素构成,但不能包含函数。
#include <iostream>
using namespace std;
struct Student{
string name;
long bir;
};
int main(){
Student s1;
s1.name="bruce";
s1.bir=19980101;
Student s2{"calvin",20021231};
return 0;
}
结构体不仅可以被声明为变量,还有指针或数组等,用以实现较复杂的数据结构:
#include <iostream>
using namespace std;
struct Student{
string name;
long bir;
};
int main(){
//结构体指针
Student s{"calvin",20021231};
Student *ptr=&s;
cout<<ptr->name<<endl;
//结构体数组
Student Class[50];
return 0;
}
C 语言提供的结构体可以用来实现较为复杂的数据结构,但不能包含函数,无法实现面向对象,即自定义数据类型及其方法。但 C++ 中的结构体可以定义函数,用法与类相似。但 struct 中默认为 public 类型,class 中默认为 private 类型。
二. 面向对象
C++ 和 C 最大的区别就是 C++ 引入了面向对象编程,即自定义类:根据需求定义类的变量和函数,将其封装成一个新的数据类型供外部调用。同时还可以通过访问级别控制访问权限,通过继承实现多态。
类对象调用属性或方法时直接使用 .
即可,指针调用时使用 ->
。
1. 成员变量与成员函数
类的定义就是定义类的成员变量与成员函数,前者描述属性,后者描述行为。
一般为了保护类的属性不被轻易修改,会将其定义成 private 属性(若想被继承则是 protected),这样只有通过对象的成员函数才可以访问与修改:
class Champion{
private:
string name;
int HP;
int damage;
public:
Champion(string name,int HP,int damage){
this->name=name;
this->HP=HP;
this->damage=damage;
}
void takeDamage(int incoming){
//成员函数修改成员变量
HP-=incoming;
}
void attack(Champion &chmp){
//访问chmp的成员属性需要调用其成员函数
chmp.takeDamage(this->damage);
}
};
并且对于一个类对象,其所占内存(栈空间)大小只与成员变量有关,因为静态成员变量存在于全局数据区,成员函数存在于代码区。若无成员变量,则自动补充 1 个字节。
成员函数可以定义在类的内部,也可以在类的内部声明外部定义,但后者需要在函数名前加上作用域:
class Champion{
private:
string name;
int HP;
int damage;
public:
Champion(string name,int HP,int damage){
this->name=name;
this->HP=HP;
this->damage=damage;
}
void takeDamage(int incoming){ //类中定义
HP-=incoming;
}
void attack(Champion &chmp); //类外定义需要先在类中声明
void printHP() const {
// this->HP=0; //常量成员函数不能修改对象的成员变量
cout<<"Current HP:"<<HP<<endl;
}
};
void Champion::attack(Champion &chmp){ //类外定义时要在函数名前加类名作用域
chmp.takeDamage(this->damage);
}
为了防止意外修改成员变量,通常还会用 const 修饰不需要改变成员变量的成员函数。
2. 构造函数
构造函数与类同名,主要功能是初始化类中的成员变量,在实例化对象时会自动调用。若没有自定义构造函数,则按系统默认构造函数,将成员变量初始化为各自的默认值。而且构造函数支持重载,用于初始化不同参数数量或类型的对象。构造函数常常采用初始化列表以节省开销。
class Area{
private:
int area;
public:
Area(){ //默认构造函数
this->area=0;
}
Area(int a){ //重载构造函数
this->area=a*a;
}
Area(int a,int b){
this->area=a*b;
}
// Area(int a,int b):area(a*b){} //初始化列表
};
构造函数还有一种使用场景,就是将函数中的参数隐式转换成类对象,但这种用法仅限于构造函数只有一个参数。如果不希望隐藏的构造函数调用,可以使用 explicit 关键字修饰,这样声明的构造函数只能被显示调用:
#include <iostream>
#include <vector>
using namespace std;
class MyClass1{
private:
int num;
public:
MyClass1(int n):num(n){}
int getNum(){return num;}
};
class MyClass2{
private:
int num;
public:
explicit MyClass2(int n):num(n){}
int getNum(){return num;}
};
int main(){
MyClass1 mc1=5; //构造函数隐式转换
// MyClass2 mc1=5; //禁止构造函数隐式转换
return 0;
}
3. 复制构造函数
当类对象需要复制变量时,就需要编译器使用现有对象的成员数据重新构造一个临时对象,这样的构造函数就叫作复制构造函数。在复制原对象构造新的对象、函数传参、数组存值等情况会使用复制构造函数:
#include <iostream>
#include <vector>
using namespace std;
class MyClass{
private:
int a;
public:
MyClass(int a):a(a){}
MyClass(const MyClass &myclass):a(myclass.a){
cout<<"Copy Construction!"<<endl;
}
};
void test(MyClass mc){
return;
}
int main(){
MyClass mc1(5);
MyClass mc2(mc1); //复制构造
test(mc2); //函数传参
MyClass arr[2]={mc1,mc2}; //数组存值
/*Copy Construction!
Copy Construction!
Copy Construction!
Copy Construction!
*/
return 0;
}
注意,复制构造函数的参数是该类的一个对象的引用,可以不加 const 修饰,但必须要加引用符!!!否则复制构造函数在调用时将无限循环。
如果不想让对象在函数中按值传递,以免造成无休止的复制,可以将复制构造函数设为 private。
其实复制构造函数在没有定义的情况下也会由编译器自动合成,但只能执行浅拷贝,即简单的复制所有成员(包括类对象,调用其复制构造函数,若无则自动合成)。当类成员属性中出现指针时,合成的复制构造函数只会复制指针的值,即指针所指向的地址,而不是创建一个新的地址赋给属性指针。这样的坏处是复制后的新对象与原对象存在关联,改变任意一个都会影响另一个的值。
因此需要深拷贝,在复制指针时动态分配一个新的指针存放待复制指针的解引用值,即找了一个新的地址将待复制指针的信息复制到新地址处:
#include <iostream>
#include <vector>
using namespace std;
class Component{
private:
int val;
public:
Component(int v):val(v){}
int getVal(){return val;}
};
class MyClass{
private:
Component comp;
Component *compPtr;
public:
MyClass(Component cp,Component *cpPtr):comp(cp),compPtr(cpPtr){}
MyClass(MyClass &myclass):comp(myclass.comp){ //自定义复制构造函数进行深拷贝
//comp(myclass.comp) 使用编译器自动合成的Component构造函数进行浅拷贝
this->compPtr=new Component(*myclass.compPtr); //对被复制对象的指针解引用,然后创建新指针
}
~MyClass(){delete compPtr;}
void print(){
cout<<"Value of comp:"<<comp.getVal()<<endl;
cout<<"Address of compPtr:"<<compPtr<<endl;
cout<<"Value where compPtr points:"<<compPtr->getVal()<<endl;
}
};
MyClass test(MyClass mc){
return mc;
}
int main(){
Component comp1(1);
Component comp2(3);
MyClass mc(comp1,&comp2);
mc.print();
test(mc).print();
/*Value of comp:1
Address of compPtr:0x6ffdd0
Value where compPtr points:3
Value of comp:1
Address of compPtr:0x9a1740
Value where compPtr points:3
*/
return 0;
}
下图可以直观表现浅拷贝和深拷贝的区别:
4. 析构函数
构造函数用来初始化对象,析构函数用来释放内存。在 main() 函数结束 或 delete 对象指针 时自动调用释放内存。析构函数一般只用处理数组和类对象,基本数据类型不需要其释放。
class MyClass{
private:
int size;
int *arr; //数组定义成指针即可
public:
MyClass(int size):size(size){
arr=new int[size];
for(int i=0;i<size;i++){arr[i]=i;}
}
~MyClass (){
delete[] arr; //只需要释放数组
}
};
5. this 指针
前面构造函数中已经出现了用 this 指针初始化成员变量的情形,其实 this 指针指向的就是调用成员函数的对象本身,对 this 解引用就可以得到当前对象的副本。
#include <iostream>
using namespace std;
class MyClass{
private:
int a;
int b;
public:
MyClass(int a,int b){
this->a=a;
this->b=b;
}
MyClass *getAddr(){
return this; //返回的是该对象的地址
}
MyClass getCopy(){
return *this; //返回的是该对象的副本
}
MyClass& getCopy(){
return *this; //返回的是该对象的引用,即该对象本身
}
void aPlus(){
this->a++;
}
int getA(){
return a;
}
};
int main(){
MyClass mc(2,5);
MyClass *ptr=mc.getAddr();
MyClass copy=mc.getCopy();
ptr->aPlus();
cout<<ptr->getA()<<endl; //3
cout<<copy.getA()<<endl; //2
return 0;
}
6. 类的静态成员
前面介绍过函数的静态局部对象,同样类也可以拥有其静态成员。其生命周期与 类(而不是对象)绑定,可以用于 统计实例对象的个数。
类的静态成员变量不能在构造函数中初始化,而是在类外通过类的作用域限定符初始化。类的静态成员函数一般用于返回静态成员变量,达到计数递增的效果。调用时由于在类外,需要加类的作用域限定符。
#include <iostream>
using namespace std;
class Student{
private:
int id;
string name;
static int ID; //静态成员变量
public:
Student(int id,string name):id(id),name(name){}
int getId(){
return id;
}
string getName(){
return name;
}
static int generateID(){
return ID++;
}
};
int Student::ID=0; //类外初始化静态成员变量时不能省略变量类型
int main(){
Student s1(Student::generateID(),"Anderson"); //类的作用域限定符调用静态成员函数
Student s2(Student::generateID(),"Latty");
return 0;
}
类的静态成员还可以加上 const 修饰,声明一种类的常量静态成员变量,可以直接在类内初始化:
class Student{
private:
string name;
int score;
const static int fullScore; //常量静态成员变量
public:
Student(string name,int sc):name(name){
if(sc>fullScore){
cout<<"Out of range! Ajusted to Max."<<endl;
score=fullScore;
}else{
score=sc;
}
}
};
7. 可变数据成员
类的静态成员可以统计类的被访问次数,对象的可变数据成员可以统计对象的被访问次数。可变数据成员需要用关键字 mutable
修饰:
#include <iostream>
#include <string>
using namespace std;
class Product{
public:
Product(int i, string n, int p, int w): id(i),name(n),price(p),weight(w) {views = 0;}
void checkInfo() const{
cout <<"查看商品信息: "<< endl;
cout <<"商品号: "<< id << endl;
cout <<"商品名: "<< name << endl;
cout <<"价格: "<< price <<"元"<< endl;
cout <<"重量: "<< weight <<"克"<< endl;
cout << "查看次数: "<< ++views << endl ;
cout << endl ;
}
private :
int id;
string name;
int price;
int weight;
mutable int views; //可变数据成员
};
int main(){
Product prod1(1, "辣条", 3, 50);
Product prod2(2, "辣条", 3, 50);prod1.checkInfo();
prod2.checkInfo();
prod1.checkInfo() ;
prodl.checkInfo();
return 0;
}
8. 继承
对象间的父子关系就是继承,派生类(父类)继承基类(子类)允许继承的成员属性与方法。
(1)构造与析构函数
派生类的构造函数需要在初始化列表中调用相应的基类构造函数即可;析构函数一般不需要继承,让系统自动调用即可。一个派生类对象从构造到销毁调用函数的顺序为:基类构造函数 -> 派生类构造函数数 -> 派生类析构函数 -> 基类析构函数,因为派生类对象可能依赖基类:
#include <iostream>
#include <vector>
using namespace std;
class Base{
protected:
int n;
char c;
public:
Base(int n):n(n),c('0'){
cout<<"Base constructor."<<endl;
}
Base(char c):n(0),c(c){
cout<<"Base constructor."<<endl;
}
~Base(){
cout<<"Base destructor."<<endl;
}
};
class Derived:public Base{
private:
bool b;
public:
Derived(int n,bool b=0):Base(n),b(b){
cout<<"Derived constructor."<<endl;
}
Derived(char c,bool b=0):Base(c),b(b){
cout<<"Derived constructor."<<endl;
}
~Derived(){
cout<<"Derived destructor."<<endl;
}
};
int main(){
Derived d(5);
/*
Base constructor.
Derived constructor.
Derived destructor.
Base destructor.
*/
return 0;
}
(2)访问控制
类中可以通过 public、protected、private
这 3 个访问控制符来控制成员、派生类以及外部对象对内部成员的访问,以隐藏私有的实现细节,暴露适当的用户接口。一般情况下,成员属性 或 类内辅助函数 用 private
修饰,只允许类内成员和友元访问;若有 派生类需要继承基类的成员属性,则用 protected
修饰;对于成员函数,一般用 public
修饰,允许继承和类外对象访问。
在派生类定义中,会在继承基类时加上访问控制符 public、protected、private
,进一步限制基类成员在派生类中呈现的访问权限,即派生类的成员的访问权限:
\继承方式 基类访问级别\ | private | protected | public |
---|---|---|---|
private | private | private | private |
protected | private | protected | protected |
public | private | protected | public |
上表即为从基类继承来的成员在该派生类的继承方式下的成员访问级别。
class Base{
private:
int a; //派生类不可继承,类外不可访问
protected:
int b; //派生类可继承,类外不可访问
public:
int c; //派生类可继承,类外可访问
Base():a(0),b(0),c(0){}
};
class Derived1:public Base{
//公有继承,变量b在Derived1中访问属性为protected,c为public,可以继续继承
public:
Derived1():Base(){}
// int getA(){return a;} //private属性不可继承
int getB(){return b;}
int getC(){return c;}
};
class Derived2:protected Base{
//保护继承,变量b和c在Derived2中访问属性为protected,可以继续继承
public:
Derived2():Base(){}
int getB(){return b;}
int getC(){return c;}
};
class Derived3:private Base{
//私有继承,变量b和c在Derived2中访问属性为private,不能被继承
public:
Derived3():Base(){}
int getB(){return b;}
int getC(){return c;}
};
(3)派生类与基类转换
派生类放入基类容器时会发生强制转换,派生类中新增的属性和方法将被截断:
#include <iostream>
#include <vector>
using namespace std;
class Base{
protected:
int b;
public:
Base():b(1){}
};
class Derived:public Base{
private:
int d; //新增成员
public:
Derived():Base(),d(2){}
void SaySomething(){
cout<<"This is a new function!"<<endl;
}
};
int main(){
vector<Base> baseV;
Base b;
Derived d;
cout<<"Size of b:"<<sizeof(b)<<endl; //Size of b:4
cout<<"Size of d:"<<sizeof(d)<<endl; //Size of d:8
baseV.push_back(b);
baseV.push_back(d);
cout<<"Size of baseV[0]:"<<sizeof(baseV[0])<<endl; //Size of baseV[0]:4
cout<<"Size of baseV[1]:"<<sizeof(baseV[1])<<endl; //Size of baseV[0]:4
d.SaySomething(); //This is a new function!
// baseV[1].SaySomething(); //[Error] 'class Base' has no member named 'SaySomething'
return 0;
}
基类由于缺少派生类特有的成员,无法直接转换为派生类。但可以通过派生类指针指向原本被基类指针指向的派生类对象的方法进行转换:
#include <iostream>
#include <vector>
using namespace std;
class Base{
protected:
int b;
public:
Base():b(1){}
};
class Derived:public Base{
private:
int d;
public:
Derived():Base(),d(2){}
};
int main(){
Derived d;
Base *bptr=&d;
Derived *dptr=(Derived*)bptr;
return 0;
}
(4)多重继承
当一个派生类想要继承多个基类时,可以使用多重继承。派生类的构造函数初始化列表中需要包含几个基类的构造函数:
class Student{
protected:
string school;
public:
Student(string sc):school(sc){}
string getSchool(){return school;}
};
class Child{
protected:
int father;
int mother;
string location;
public:
Child(int f,int m,string loc):father(f),mother(m),location(loc){}
string getLocation(){return location;}
};
class Citizen{
protected:
string city;
public:
Citizen(string st):city(st){}
string getCity(){return city;}
};
class Person:public Student,public Child,public Citizen{
private:
string NO;
public:
Person(string sc,int f,int m,string loc,string st):Student(sc),Child(f,m,loc),Citizen(st){}
void getInformation(){
cout<<"I'm a Student from "<<school<<".\n";
cout<<"My family is in "<<location<<".\n";
cout<<"I come from "<<city<<".\n";
}
};
(5)菱形继承
多重继承有一种特殊情况,即派生类想要继承的基类之间已经存在了继承关系,这会导致派生类继承的基类不明确,并且拥有重复的成员。
图中菱形继承的情况需要 Derived1 和 Derived2 采用虚继承 继承Base类,这样可以在间接继承共同基类时只保留一份基类成员:
class CRAFT{
public:
CRAFT(double speed):Speed(speed){
cout<<"创建航行器(速度: "<<Speed<<")"<<endl;
};
virtual ~CRAFT(){
cout<<"销毁航行器(速度: "<<this->Speed<<")"<<endl;
}
virtual void Show() const{
cout<<"航行(速度: "<<this->Speed<<")"<<endl;
}
protected:
double Speed;
};
class PLANE:virtual public CRAFT { //虚继承基类
public:
PLANE(double speed,double width):CRAFT(speed),Width(width){
cout<<"创建飞机(翼展: "<<this->Width<<")"<<endl;
};
virtual ~PLANE(){
cout<<"销毁飞机(翼展: "<<this->Width<<")"<<endl;
}
virtual void Show() const{
cout<<"航行(速度: "<<this->Speed<<", 翼展: "<<this->Width<<")"<<endl;
}
protected:
double Width;
};
class SHIP:virtual public CRAFT { //虚继承基类
public:
SHIP(double speed,double depth):CRAFT(speed),Depth(depth){
cout<<"创建船(吃水: "<<this->Depth<<")"<<endl;
};
virtual ~SHIP(){
cout<<"销毁船(吃水: "<<this->Depth<<")"<<endl;
}
virtual void Show() const{
cout<<"航行(速度: "<<this->Speed<<", 吃水: "<<this->Depth<<")"<<endl;
}
protected:
double Depth;
};
class SEAPLANE:public PLANE,public SHIP { //只写直接继承的类
public:
SEAPLANE(double speed,double width,double depth):CRAFT(speed),PLANE(speed,width),SHIP(speed,depth){
cout<<"创建水上飞机"<<endl;
};
virtual ~SEAPLANE(){
cout<<"销毁水上飞机"<<endl;
}
virtual void Show() const{
cout<<"航行(速度: "<<Speed<<", 翼展: ";
cout<<this->Width<<", 吃水: "<<this->Depth<<")"<<endl;
}
};
虽然虚继承可以解决菱形继承的二义性和冗余,但相当不建议设计出多继承,更不要设计出菱形继承。虚继承解决菱形继承原理见 C++菱形继承中的那些事【超详细 图文+代码】。
9. 虚函数和多态
在继承的过程中,当基类拥有多个不同的派生类,并且这些派生类想要继承基类的同一个方法去实现不同的功能,就需要将基类中的方法声明为虚函数。上述为不同数据类型(派生类)提供统一接口的过程称为多态。
(1)虚函数
虚函数其实很简单,只用在需要重写的函数前加上关键字 virtual
,其中基类中该函数前 virtual
关键字为必须,派生类中可以省略。由于多态的出现,面向对象设计时经常只使用一个基类指针,每次将其指向一个新创建的派生类对象调用其虚函数版本(其他非继承函数无法调用,重写的继承非虚函数也只会出现基类对象中函数),使用完该对象后切记释放该对象的内存空间,但该基类指针仍存在,可以继续指向下一个派生类对象。
#include <iostream>
using namespace std;
class CAR{
public:
CAR(double speed):Speed(speed){
cout<<"创建汽车("<<Speed<<"公里/小时)"<<endl;
};
virtual ~CAR(){ //虚函数继承体系要用虚析构函数
cout<<"销毁汽车("<<this->Speed<<"公里/小时)"<<endl;
}
virtual void Show(){ //虚函数实现多态
cout<<"汽车: "<<this->Speed<<"公里/小时"<<endl;
}
protected:
double Speed;
};
class BUS:public CAR {
public:
BUS(double speed,int seat):CAR(speed),Seat(seat){
cout<<"创建客车("<<this->Seat<<"人)"<<endl;
};
virtual ~BUS(){
cout<<"销毁客车("<<this->Seat<<"人)"<<endl;
}
virtual void Show(){
cout<<"客车: "<<this->Speed<<"公里/小时, "<<this->Seat<<"人"<<endl;
}
protected:
int Seat;
};
class TRUCK:virtual public CAR {
public:
TRUCK(double speed,double load):CAR(speed),Load(load){
cout<<"创建货车("<<this->Load<<"吨)"<<endl;
};
virtual ~TRUCK(){
cout<<"销毁货车("<<this->Load<<"吨)"<<endl;
}
virtual void Show(){
cout<<"货车: "<<this->Speed<<"公里/小时, "<<this->Load<<"吨"<<endl;
}
protected:
double Load;
};
int main(){
CAR *car=new BUS(60.0,30); //基类指针指向新创建的派生类对象
car->Show(); //调用基类对象的虚函数版本
delete car; //重新赋值前一定要销毁BUS对象
car=new TRUCK(40,2);
car->Show();
delete car;
/*创建汽车(60公里/小时)
创建客车(30人)
客车: 60公里/小时, 30人
销毁客车(30人)
销毁汽车(60公里/小时)
创建汽车(40公里/小时)
创建货车(2吨)
货车: 40公里/小时, 2吨
销毁货车(2吨)
销毁汽车(40公里/小时)
*/
return 0;
}
不过需要注意的是,继承体系中有虚函数时需要使用虚析构函数。因为使用基类指针指向派生类对象的写法在 delete car
时只会调用基类析构函数,因此需要使用存放在类的开头的虚析构函数覆盖基类的虚构函数,从而使得基类指针得以调用。
上面讲到基类指针可以调用派生类同名虚函数,但如果其想调用基类中该函数,则需要加上作用域操作符:
car->CAR::Show();
(2)纯虚函数
上面例子中的基类虚函数也被定义了操作,但现实中经常遇到基类是抽象类,并没有实际行为。对于这种情况,会在基类虚函数后加上 =0
构成纯虚函数,这样就可以跳过函数实现。这时的类只是一个抽象类,不能够实例化,仅为其派生类提供一个统一接口。
#include <iostream>
using namespace std;
#define PI 3.14159f;
class Shape{
public:
Shape(){}
virtual double Area() = 0; //纯虚函数
};
class Circle:public Shape{
public:
Circle(double r):r(r){}
double r;
virtual double Area(){
return r*r*PI; //编译器误将PI*r*r中*认为是指针解引用
}
};
class Square:public Shape{
public:
Square(double a):a(a){}
double a;
virtual double Area(){
return a*a;
}
};
int main(){
double r,a;
cin>>r>>a;
Shape *shape = new Circle(r);
double Area1=shape->Area();
delete shape;
shape = new Square(a);
double Area2=shape->Area();
delete shape;
printf("%.3f",Area1+Area2);
return 0;
}
10. 重载操作符
在面向对象设计中,有时需要使用C++已有操作符,这个时候就需要重载操作符。操作符重载分为类成员形式和非成员形式,前者以类的成员函数形式出现,后者在类外以全局函数形式出现。重载操作符规则如下:
- 重载的操作符必须合法,且重载时至少有一个类类型的操作数;
- 重载操作符只改变其原有变量类型,不能修改该操作符的操作数数量、优先级、结合性等;
- 尽量不重载具有系统内置意义的操作符,如取址、逗号等。
(1)赋值操作符
重载赋值操作符时需要返回类对象的引用:
#include <iostream>
using namespace std;
class Component{
private:
int val;
public:
Component(int v):val(v){}
int getVal(){return val;}
};
class MyClass{
private:
Component *compPtr;
public:
MyClass(int val){
compPtr=new Component(val);
}
~MyClass(int val){
delete compPtr;
}
MyClass& operator=(const MyClass &rhs){ //重载赋值操作符
*comPtr=*rhs.compPtr;
return *this; //返回该对象的引用
}
};
int main(){
MyClass mc1(1);
MyClass mc2(2);
mc2=mc1; //重载赋值操作符传值
MyClass mc3=mc1; //调用复制控制函数创建对象
return 0;
}
需要注意的是,重载赋值操作符不等于复制控制函数,前者是将一个对象赋值给另一个已经创建的对象,后者是用一个对象构造一个新的对象。
(2)算术操作符
算术操作符一般写做非成员形式,并且遇到操作数类型不同时需要重复定义以保证运算符的对称性,比如:Vector*2
和 2*Vector
。
class Point{
public:
Point(double x,double y):X(x),Y(y){};
/*
Point operator+=(const Point &p2){ //成员形式的可以返回对象本身
X+=p2.X;
Y+=p2.Y;
return *this;
}
void operator-=(const Point &p2){ //成员形式也可以无返回值
X-=p2.X;
Y-=p2.Y;
}
*/
double X;
double Y;
};
//非成员形式要有返回值
Point operator+(const Point &p1,const Point &p2){
Point p3(0,0); //创建新的对象并返回
p3.X=p1.X+p2.X;
p3.Y=p1.Y+p2.Y;
return p3;
}
Point operator-(const Point &p1,const Point &p2){
Point p3(0,0);
p3.X=p1.X-p2.X;
p3.Y=p1.Y-p2.Y;
return p3;
}
Point operator+=(Point &p1,const Point &p2){
p1.X+=p2.X; //需要修改p1,因此p1需要引用但不能是const修饰
p1.Y+=p2.Y;
return p1;
}
Point operator-=(Point &p1,const Point &p2){
p1.X-=p2.X;
p1.Y-=p2.Y;
return p1;
}
当算术操作符不改变操作数的值时,采用的写法是创建新的返回值,重载函数的参数使用const引用。
(3)关系操作符
关系操作符既可以是成员形式,也可以是非成员形式:
class TIME{
public:
TIME(int hour=0,int minute=0,int second=0): //构造默认值
Hour(hour),Minute(minute),Second(second){};
bool operator>(const TIME&b){ //TIME和TIME比较
int s1=this->Hour*60*60+this->Minute*60+this->Second;
int s2=b.Hour*60*60+b.Minute*60+b.Second;
if(s1>s2)
return true;
else
return false;
}
int Hour;
int Minute;
int Second;
};
bool operator>(int h,const TIME&t){ //int和TIME比较
return TIME(h)>t;
}
(4)类型转换操作符
类型转换操作符不改变原有对象,一般声明为const类型函数。又由于操作符名就是返回值类型名,因此 函数名前省略返回值类型。并且类型转换操作符的重载函数必须声明为 成员函数 且 形参列表为空。
#include <iostream>
using namespace std;
class Complex{
public:
Complex():real(0),imag(0){}
Complex(double r,double i):real(r),imag(i){}
operator double() const{ //函数名前省略返回值类型,且形参列表为空
return real;
}
private:
double real;
double imag;
};
int main(){
Complex c(3,-4);
double d1=5;
double d2=d1+double(c);
cout<<d2<<endl;
return 0;
}
(5)自增自减操作符
C++规定的自增操作符分为前缀和后缀(自减同自增),前缀操作符先自增再返回自增后的值,后缀操作符先返回值再自增,因此前缀自增操作符重载时返回的是对象的引用,后缀自增操作符需要先创建旧对象的副本再对 this 指针指向的对象进行自增,然后返回副本。此外,为了区分前后缀操作符的重载函数,规定在后缀版本的参数列表中加入占位符 int
,但无法在函数中使用:
#include <iostream>
using namespace std;
class TIME{
public:
TIME(int hour=0,int minute=0,int second=0):
Hour(hour),Minute(minute),Second(second){};
TIME(const TIME &time){
Hour=time.Hour;
Minute=time.Minute;
Second=time.Second;
}
TIME& operator++(){ //前缀自增操作符重载
if(Second==59){
Second=0;
if(Minute==59){
Minute=0;
if(Hour==23){
Hour=0;
}else{
Hour++;
}
}else{
Minute++;
}
}else{
Second++;
}
cout<<"前缀自增操作符重载"<<endl;
cout<<"现在是"<<Hour<<"时"<<Minute<<"分"<<Second<<"秒"<<endl;
return *this; //返回对象的引用
}
TIME operator++(int){ //后缀自增操作符重载
TIME old(*this); //创建旧对象的副本便于返回返回值
if(Second==59){
Second=0;
if(Minute==59){
Minute=0;
if(Hour==23){
Hour=0;
}else{
Hour++;
}
}else{
Minute++;
}
}else{
Second++;
}
cout<<"后缀自增操作符重载"<<endl;
cout<<"现在是"<<Hour<<"时"<<Minute<<"分"<<Second<<"秒"<<endl;
return old; //返回旧对象的副本
}
private:
int Hour;
int Minute;
int Second;
};
int main(){
TIME time(23,59,59);
time++;
return 0;
}
(6)输入输出操作符
对象也可以通过重载输入输出操作符实现输入输出,且模式较为固定,遵守即可:
class TIME{
public:
TIME(int hour=0,int minute=0,int second=0):
Hour(hour),Minute(minute),Second(second){};
int Hour;
int Minute;
int Second;
};
ostream& operator<<(ostream &os,const TIME &t){ //输入输出操作符重载只能在类外定义
if(t.Hour<10) //补零
os<<"0"<<t.Hour<<":";
else
os<<t.Hour<<":";
if(t.Minute<10)
os<<"0"<<t.Minute<<":";
else
os<<t.Minute<<":";
if(t.Second<10)
os<<"0"<<t.Second;
else
os<<t.Second;
return os;
}
istream& operator>>(istream &is, TIME &t){
is>>t.Hour;
is.ignore(1,':'); //忽略输入“8:3:5”中的冒号
is>>t.Minute;
is.ignore(1,':');
is>>t.Second;
return is;
}
注意,输入流无法直接读取变量外的字符,需要使用 ignore
函数规范化输入。
(7)下标操作符
#include <iostream>
using namespace std;
class MyVec{
public:
MyVec(int s=0):size(s){
arr=new int[size];
for(int i=0;i<s;i++){
arr[i]=i;
}
}
int operator[](int i){
if (i>=size){
cout<<"Index out of range!"<<endl;
return -1;
}else{
return arr[i];
}
}
private:
int size;
int *arr;
};
int main(){
MyVec vec(5);
cout<<vec[3]<<endl;
return 0;
}
11. 友元
前面介绍了类中的访问控制级别,但有时也会有例外,比如操作符需要以非成员形式重载时(前面示例中采取公有化类成员,但这在现实设计中相当危险),就需要重载函数能够访问类成员。还有当基类只允许特定派生类继承其私有成员或函数时,也需要灵活改变访问控制权限。这时就需要引入友元,来使得某个函数或类具有访问另一个类的私有成员的权限,友元需要在类中被声明。
(1)友元类
声明友元类时需要加 class
关键字:
class Stuff{
friend class Teacher; //声明友元类需要加class
private:
string NO;
public:
Stuff(string no):NO(no){}
};
class Student:public Stuff{
public:
Student(string no):Stuff(no){}
};
class Teacher:public Stuff{
public:
Teacher(string no):Stuff(no){}
void getStudent(const Student &student){
cout<<"Student "<<student.NO<<endl;
}
};
可以看到,在 Stuff 类中声明 Teacher 为友元类,可以让 Teacher 类访问其私有成员,而没有被声明为友元类的 Student 就不行。
(2)友元函数
单个函数也可以在类中被声明为友元,常见情况为非成员形式的操作符重载:
class Point{
//算数操作符一般写成非成员形式,声明为友元以访问X和Y
friend Point operator+(const Point &p1,const Point &p2);
friend Point operator-(const Point &p1,const Point &p2);
friend ostream&operator<<(ostream&os,const Point &t);
public:
Point(double x,double y):X(x),Y(y){};
private:
double X;
double Y;
};
Point operator+(const Point &p1,const Point &p2){
Point p3(0,0);
p3.X=p1.X+p2.X;
p3.Y=p1.Y+p2.Y;
return p3;
}
Point operator-(const Point &p1,const Point &p2){
Point p3(0,0);
p3.X=p1.X-p2.X;
p3.Y=p1.Y-p2.Y;
return p3;
}
ostream&operator<<(ostream&os,const Point &t){
os<<t.X<<","<<t.Y;
return os;
}
12. 含有指针属性的对象
当类的属性中含有指针(一般用来表示数组)时,最好自定义复制构造函数并重载赋值操作符,因为一些操作中隐式调用了复制或赋值操作。第 3 节介绍了指针赋值时深拷贝的应用,但对于数组来说,还需要将数组的元素值也进行复制:
const int MaxSize = 10000;
class user{
public:
int NO;
int* arr;
user(){
NO=0;
arr=nullptr;
}
user(int no){
NO=no;
arr=new int[MaxSize];
for(int i=0;i<MaxSize;i++){
arr[i]=0;
}
}
~user(){
delete [] arr;
}
user(const user &u){
NO=u.NO;
arr=new int[MaxSize];
for(int i=0;i<MaxSize;i++){
arr[i]=u.arr[i]; // 复制数组元素
}
}
user& operator=(const user& u){
if (this!=&u) {
NO=u.NO;
delete[] arr; // 重新分配空间前需要删除原本空间
arr=new int[MaxSize]; // 重新分配空间
for (int i=0;i<MaxSize;i++) {
arr[i]=u.arr[i]; // 复制数组元素
}
}
return *this;
}
};
三. 泛型编程
STL容器在实例化时会对其类型进行赋值,这是因为STL容器在定义时采用了泛型编程。简单来说,就是在定义类时并不确定实例化对象中元素的类型,因此在定义类时使用一个比较泛化的类型。C++ 为了使用泛型编程,引入了 模板 的概念,此处只简单介绍 类模板 和 函数模板。定义时类模板或函数模板前需要加入 template <typename T>
,调用时需要对类型 T
赋真实类型。并且模板形参也支持默认参数。
1. 类模板
其实 template <typename T>
包住的模板形参列表不一定要是类型形参,还可以是非类型形参,前者用于泛型编程时确定对象中元素类型,后者类似于函数参数,但实例化时必须确定且必须为常量。
#include <iostream>
using namespace std;
template <typename T=int,int capacity=10> //模板形参列表包含类型形参和非类型形参,支持默认值
class MyArray{
private:
T *arr;
int size;
public:
MyArray(int size){
if(size>capacity){
cout<<"Too large array!"<<endl;
exit(-1); //出错返回
}
this->size = size;
arr = new T[capacity];
for(int i=0;i<size;i++)
cin>>*(arr+i); //类定义要求初始化时输入元素
}
void sort(){
T temp;
for(int i=0;i<size-1;i++){ //冒泡排序
for(int j=0;j<size-1;j++){
if(*(arr+j)>*(arr+j+1)){
temp=*(arr+j);
*(arr+j)=*(arr+j+1);
*(arr+j+1)=temp;
}
}
}
}
void display(){ //输出函数
for(int i=0; i<size-1; i++){
cout<<*(arr+i)<<" ";
}
cout<<*(arr+size-1)<<endl;
}
~MyArray();
bool check();
};
template<typename T,int capacity> //类外定义时需要加上template和模板形参列表
MyArray<T,capacity>::~MyArray(){ //类外定义时作用域操作符前类名要加上模板形参
delete[] arr;
}
template<class T,int capacity>
bool MyArray<T,capacity>::check(){
int i;
for(i=0;i<size-1;i++){
if(arr[i]>arr[i+1]){
cout<<"ERROR!"<<endl;
return false;
}
}
cout<<"Check Passed!"<<endl;
return true;
}
int main(){
/*每行输入的第一个数字为0,1,2或3:
为0时表示输入结束;为1时表示将输入整数;为2时表示将输入浮点数;为3时表示输入字符。
如果第一个数字非0,则接下来将输入一个正整数,表示即将输入的数据的数量。
从每行第三个输入开始,依次输入指定类型的数据。
*/
const int capacity=10;
MyArray<int,capacity> *pI; //非类型形参实例化时必须确定且必须为常量
MyArray<float,capacity> *pF;
MyArray<char,capacity> *pC;
int ty, size;
cin>>ty;
while(ty>0){
cin>>size;
switch(ty){
case 1: pI = new MyArray<int,capacity>(size); pI->sort(); pI->check(); pI->display(); delete pI; break;
case 2: pF = new MyArray<float,capacity>(size); pF->sort(); pF->check(); pF->display(); delete pF; break;
case 3: pC = new MyArray<char,capacity>(size); pC->sort(); pC->check(); pC->display(); delete pC; break;
default:cout<<"Wrong Input!"<<endl; break;
}
cin>>ty;
}
return 0;
}
需要注意的是,当成员函数定义在类外时,需要 加上template和模板形参列表,并且 作用域操作符前类名要加上模板形参。
2. 函数模板
函数模板可以是普通函数:
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
template <typename T> //普通函数模板
void sortT(T* arr,int n){ //输入的第一个形参为T型指针,指代数组
for(int i=0;i<n;i++){
cin>>arr[i];
}
sort(arr,arr+n);
}
template <class T>
void display(T* arr, int size){
for(int i=0; i<size-1; i++) cout<<arr[i]<<' ';
cout<<arr[size-1]<<endl;
}
int main() {
const int SIZE=10;
int a[SIZE];
char b[SIZE];
double c[SIZE];
string d[SIZE];
int ty, size;
cin>>ty;
while(ty>0){
cin>>size;
if(size>SIZE){
cout<<"Too large array!"<<endl;
exit(-1);
}
switch(ty){
case 1:sortT<int>(a,size); display(a,size); break;
case 2:sortT<char>(b,size); display(b,size); break;
case 3:sortT<double>(c,size); display(c,size); break;
case 4:sortT<string>(d,size); display(d,size); break;
default:cout<<"Wrong Input!"<<endl; break;
}
cin>>ty;
}
return 0;
}
也可以是类成员函数,甚至是类模板的成员函数:
#include <iostream>
using namespace std;
template <typename T> //类模板
class Vector2D{
public:
Vector2D(T x=T(),T y=T()):x(x),y(y){} //x和y默认值为类型T的默认值
void print(){ //类模板的成员函数
cout<<"("<<x<<","<<y<<")"<<endl;
}
template <typename RType> //类内定义函数模板
Vector2D add(const RType &rhs){
this->x+=rhs;
this->y+=rhs;
return *this;
}
template <typename RType> //类外定义函数模板
Vector2D sub(const RType &rhs);
private:
T x;
T y;
};
template <typename T> template <typename RType> //类外定义时两个template都要写
Vector2D<T> Vector2D<T>::sub(const RType &rhs){
this->x-=rhs;
this->y-=rhs;
return *this;
}
int main() {
Vector2D<float> v(1,2); //若Vector2D为int型,则减去float也无法保留小数位
v.add<int>(5).print(); //实例化模板时需要指明类型
v.sub<float>(3.2).print();
return 0;
}
在上面的例子中,实例化 RType 时需要保证 RType 型变量支持加减法操作。