C++基础

发布时间 2023-11-30 14:14:59作者: Beasts777

文章参考:

一. C++基础

1. 一个简单的案例

#include <iostream>   //编译预处理命令
using namespace std;    //使用命名空间

int add(int a, int b);       //函数原型说明

int main()  //主函数
{
	int x, y;
	cout << "Enter two numbers: " << endl;
	cin >> x;
	cin >> y;
	int sum = add(x, y);
	cout << "The sum is : " << sum << '\n';
	return 0;
}

int add(int a, int b) //定义add()函数,函数值为整型
{
	return a + b;
}

2. 输入输出

相较于C,C++提供了更加便利的输入(cin)输出(cout)方式,其特点如下:

  • 输出自适应类型,无需像C一样规定输出的类型。
  • 可连续输入输出。
  • 依赖于<iostream>文件
  • 默认情况下,运算符>>将忽略空白符,然后读入与后面变量类型相对应的值,也就是说,>>运算符无法直接输入空格、回车符、制表符,但我们可以通过ASCII码转义来输入空格、回车符、制表符。
int x;
int y;
cin >> x >> y;      // 输入
cout << x << "\n" << y <<endl;  // 输出,endl是换行符

3. 内联函数

定义:在函数声明和定义语句之前加上inline,该函数就可以被声明为内联函数。

作用:每当程序中调用内联函数时,C++编译器就会将函数的代码替换到调用该函数的地方,从而消除调用函数时的系统开销,以提高运行速度。

注意:

  • 虽然是将代码替换过来,但对于形参的操作没有变化,除非使用指针或引用,否则依旧无法改变实参的值。
  • 内联函数内一般不要有复杂控制语句,如for、switch、while。
  • 内联函数实际上是空间换时间的操作,因此如果内联函数较长、较复杂、调用较为频繁,则不建议使用。
  • 内联函数可以在一定程度上代替宏定义,从而消去宏定义的不安全性。

4. 带有默认值参数的函数

说明:C++中函数的参数域可以带有默认值,在进行函数调用时,如果传入的实参个数小于函数的形参个数,那么将从左到右,先将将传入的实参值给到形参,然后剩下的形参取默认值。

EG:

// 参数默认值
void defaultParam(int a, int b=10, int c=10);
void defaultParam(int a, int b, int c){
    cout << "a==" << a << "\r" 
         << "b==" << b << "\r"
         << "c==" << c << endl;
}

注意:

  • 在函数原型中,所有有默认值的参数都必须位于没有默认值的参数的右边。

  • 在函数调用时,若某个参数省略,则其后的参数皆应省略而采取默认值。不允许某个参数省略后,再给其后的参数指定参数值。

  • 一个函数可以经过多次声明,但在给定的作用域内一个形参只能被赋予一次默认实参,也就是说,函数在后续声明中只能赋予没有默认值的形参以默认实参,且该形参的右侧所有形参必须都有默认值。同时也要注意函数定义时也是如此(但函数定义只能定义一次)。

    int f(int a,int b,int c,int d); 	//第一次声明
     
    int f(int a,int b,int c,int d=0);	//第二次声明,添加默认实参d
     
    int f(int a,int b,int c=1,int d);	//第三次声明,注意此处不能再次赋予形参d一个值
     
    int f(int a=4,int b=3,int c,int d); //第四次声明,注意一旦形参a赋值后,其后所有值都必须有默认值,且不能重复赋值已有实值的形参
     
    //函数定义,注意在定义时不能再次给已有默认值的形参赋值
    int f(int a,int b,int c,int d){/**/};
     
    //虽然函数可以多次声明,但是只能定义一次。
    int ff(int m=0,int n=1,int p=2){/**/};
    
  • 带默认值的函数和函数重载一起使用,可能会引发二义性。

    void Drawcircle(int r = 0, int x = 0, int y = 0);
    void Drawcircle(int r);
    Drawcircle(20);
    

5. 函数重载

定义:在同一作用域内,只要函数的参数列表不同(包括参数种类不同,或参数个数不同,或二者兼有),那么这些函数就可以使用相同的函数名。

void ioFun();
void ioFun(int a);
// double ioFun(); 会报错,因为函数重载条件不包括返回值类型。

注意:

  • 函数重载对函数返回值不敏感,对参数名也不敏感,因此无法通过它们实现函数重载。
  • 函数重载与带默认值的函数一起使用,有可能引起二义性。

6. new和delete

说明:不同于C的malloc()free(),C++提供了运算符newdelete来对堆区进行动态内存管理,且使用更加方便。

EG:

void assignMemory(){
    // 为基础类型分配空间并初始化
    int *a = new int(99);   // 为a分配sizeof(int)的空间并将其初始化为99
    cout << *a <<endl;
    delete a;               // 释放sizeof(int)的空间
    // 数组分配空间并初始化
    int *p = new int[10];       // 为p分配sizeof(int)*10的空间,未初始化
    for (int i = 0; i < 10; i++)
    {
        p[i] = i;
    }
    delete []p;                 // 释放p指向的数组的空间
}

注意:

  • 当内存分配失败时,new会返回空指针NULL
  • 分配的内存一定要记得释放

7. 引用(reference)

说明:引用就是为变量起别名,当使用该别名时,操作的空间和原变量的空间地址一模一样,省略了使用指针操作时的解引用过程。

语法:

类型 &引用名 = 已定义的变量名

注意:

  • 引用本身并不是一种数据类型,它的存在依赖于其它变量,因此引用在声明时就必须初始化,不能先声明再初始化
  • 不能建立起void类型的引用。
  • 因为引用本身不是一种数据类型,必须依存于其它的是数据类型,因此不能建立起引用的引用,也不能有指向引用的指针。
  • 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。
  • 和指针不同,指针本身就是一种基础数据类型,用于保存地址,大小固定为8个字节;而引用的大小则随着其相连变量的大小而变化,如应用int类型,那就是4个字节,引用long long int类型,就是8个字节。

8. NULL和nulptr

C使用NULL表示空,其定义为:

#define NULL ((void *)0)

在C++中同样定义了NULL,但和C不同,C++是强类型语言,不允许隐式的类型转化,因此如果沿袭C中NULL的定义,那么下述代码会出错:

int *p = NULL;

因此C++对NULL进行了新的定义:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

也就是说,如果定义了__cplusplus宏,那么NULL就是0,否则就依旧是((void *)0),因此在C++中,NULL实际上就是0。

但是,如果令NULL==0,有时会产生二义性,例如如下代码:

#include <iostream>
using namespace std;

void func(void* i){cout << "func1" << endl;}
 
void func(int i){cout << "func2" << endl;}
 
void main(int argc,char* argv[])
{
	func(NULL);
	getchar();
}

这时会输出:func2。显然,二义性产生了。

为了解决这一问题,我们可以使用如下定义:

const class nullptr_t
{
public:
    template<class T>
    inline operator T*() const
        { return 0; }
 
    template<class C, class T>
    inline operator T C::*() const
        { return 0; }
 
private:
void operator&() const;
} nullptr = {};

而在C++11中,定义了nullptr,使用它来表示空指针。

总之,在C++中,如果要表示空指针,建议使用nullptr而不是NULL

二. 类和对象

相较于C语言的面向过程编程,C++改为面向对象编程,万物皆对象。

1. 构成

类一般由两部分构成:

  • 数据成员
  • 成员函数

访问权限可划分为三种,不同权限对应着不同的访问和继承规则:

  • public
  • protectd
  • private

一个简单的对象:

#include <iostream>
using namespace std;

class Student
{
private:					// 参数域
    long long id;
    int age;
    char* name;
    char sex;
public:	
    Student();      				// 空的构造函数    
    Student(long long id, int age, char* name, char sex);   // 构造函数
    ~Student();                 // 析构函数:用于删除对象

    void setId(long long id);
    long long getId();
};

Student::Student(){
    cout<<"这是空的构造函数"<<endl;
}

Student::Student(long long id, int age, char* name, char sex)
{
    this->id = id;
    this->age = age;
    this->name = name;
    this->sex = sex;
}

Student::~Student()
{
    delete this->name;  // 释放内存
}

void Student::setId(long long id){
    this->id = id;
}

long long Student::getId(){
    return this->id;
}


int main(){
    char* name = "zhangSan";
    Student* stu = new Student(123456789l, 18, name, '1');
    cout<< stu->getId() <<endl;
    return 0;
}

2. 成员函数

2.1 普通成员函数

定义:普通成员函数在类内部声明,然后在类外部完成函数的定义。注意在类外部定义类的成员函数时,需要以作用域来取ClassName::function()

EG:

class Student
{
private:
    long long id;
    int age;
    char* name;
    char sex;

public:         
    Student(long long id, int age, char* name, char sex);   // 构造函数
    ~Student();                 // 析构函数:用于删除对象

    void setId(long long id);
    long long getId();
};

Student::Student(){
    cout<<"这是空的构造函数"<<endl;
}

Student::Student(long long id, int age, char* name, char sex)
{
    this->id = id;
    this->age = age;
    this->name = name;
    this->sex = sex;
}

Student::~Student()
{
    delete this->name;  // 释放内存
}

void Student::setId(long long id){
    this->id = id;
}

long long Student::getId(){
    return this->id;
}

注意:

  • 必须是在类外定义,如果在类内部定义,那就是内联函数了。

2.2 内联成员函数

说明:所谓内联成员函数,和内联函数的效果一样,使用该函数的地方编译器会将该函数的代码复制过去,从而提高效率,用空间换时间,只不过这个内联函数是类的成员函数而已。

定义:分为两种:

  • 隐式声明:将成员函数直接定义在类内部,就像Java里一样,这样默认就是内联成员函数。

    class Score{
    private:
    	int mid_exam;
    	int fin_exam;
    public:
    	void setScore(int m, int f)		// 定义在内部,所以是内联函数
    	{
    		mid_exam = m;
    		fin_exam = f;
    	}
    	void showScore()
    	{
    		cout << "期中成绩: " << mid_exam << endl;
    		cout << "期末成绩:" << fin_exam << endl;
    	}
    };
    
  • 显式声明:在类外声明某成员函数时在前面加上inline关键字,从而显式声明其为内联成员函数。

    class Score{
    private:
    	int mid_exam;
    	int fin_exam;
    public:
    	void setScore(int m, int f);
    	void getScore();
    };
    inline void Score::setScore(int m, int f){
        mid_exam = m;
    	fin_exam = f;
    }
    inline void Score::getScore(){
        cout << "期中成绩: " << mid_exam << endl;
    	cout << "期末成绩:" << fin_exam << endl;
    }
    

注意:在使用inline定义内联成员函数时,必须将类的声明和定义放在同一个文件(或同一个头文件内),否则编译时将无法进行代码替换。

2.3 构造函数和析构函数

2.3.1 构造函数

说明:构造函数是一种特殊的成员函数,其存在的意义在于为对象分配空间,完成对象的初始化。

特点:

  • 没有返回值。
  • 命名与类一致。
  • 参数随意,可以不带。
  • 无需用户调用,建立对象时会自动调用。
  • 如果没有创建,编译器会自动生成一个无参构造函数。
  • 对于没有定义构造函数的类,可以使用成员初始化列表初始化公有成员

EG:

class Student
{
private:
    long long id;
    int age;
    char* name;
    char sex;

public:    
    Student();
    Student(long long id, int age, char* name, char sex);   // 构造函数
};

Student::Student(){
    cout<<"这是空的构造函数"<<endl;
}

Student::Student(long long id, int age, char* name, char sex)
{
    this->id = id;
    this->age = age;
    this->name = name;
    this->sex = sex;
}

成员初始化列表:

在对类进行初始化时,可以在构造函数内依次对成员赋值,也可以使用成员初始化列表对其进行赋值。

语法:

类名::构造函数名([参数表])[:(成员初始化列表)]{
    // 函数体
}

EG:

class Student
{
private:
    long long id;
    int age;
    char* name;
    char sex;

public:    
    Student();
    Student(long long id, int age, char* name, char sex);   // 构造函数
    Student(long long newId, int newAge, char* newName) : id(newId), age(newAge), name(newName), sex('1'){
        cout << "使用成员初始化列表进行初始化" << endl;
    }
};

Student::Student(){
    cout<<"这是空的构造函数"<<endl;
}

Student::Student(long long id, int age, char* name, char sex)
{
    this->id = id;
    this->age = age;
    this->name = name;
    this->sex = sex;
}

注意:类成员是按照它们在类里面被声明的顺序初始化的,与其在成员初始化列表里面的顺序无关。

2.3.2 析构函数

说明:析构函数是用来撤销对象的,主要负责释放对象内部成员的空间,完成一些连带业务等。

特点:

  • 名字与类名相同,但前面必须加一个~
  • 没有返回值和参数,无法重载。
  • 无需用户调用,当撤销对象时,编译系统会自动调用析构函数。
  • 如果没有定义,编译器将会创建一个默认的析构函数。

EG:

#include <iostream>
using namespace std;

class Student
{
private:					// 参数域
    long long id;
    int age;
    char* name;
    char sex;
public:	
    ~Student();                 // 析构函数:用于删除对象
};

Student::~Student()
{
    delete this->name;  // 释放内存
}

int main(){
    char* name = "zhangSan";
    Student* stu = new Student(123456789l, 18, name, '1');
    cout<< stu->getId() <<endl;
    return 0;
}

调用逻辑:

  • 如果是一个全局对象,当离开其作用域时,自动调用。
  • 如果定义在函数内,函数结束时,自动调用。
  • 如果使用new创建出来的,使用delete手动释放。

2.3.3 拷贝构造函数

说明:是一种特殊的构造函数,但其形参为本类对象的引用,其本质上是根据一个已经存在的对象,来拷贝出一个新的对象。

EG:

#include <iostream>
using namespace std;

class Score
{
private:
    int mid_exam;
    int fin_exam;
public:
    Score(const Score &score);  // 拷贝构造函数
    
    int getMidExam();
};

Score::Score(const Score &score){
    this->mid_exam = score.mid_exam;
    this->fin_exam = score.fin_exam;
}

int Score::getMidExam(){
    return this->mid_exam;
}

int main(int argc, char* argv[]){
    // 在C++中,NULL被强定义为0,在重载时有时会产生二义性。因此推荐使用nullptr,表示空指针。
    Score *p = nullptr;    
    Score s1(80, 90);
    Score s2(s1);
    cout << s1.getMidExam() << endl;    // 80
    cout << s2.getMidExam() << endl;    // 80

    return 0;
}

注意:

  • 在拷贝构造函数中,可以直接访问传入对象的所有属性,即使是private级别也可以直接访问。

浅拷贝与深拷贝:

拷贝构造函数分为两种:浅拷贝和深拷贝。

  • 浅拷贝:这也是默认的拷贝构造函数。直接将传入对象的值依次赋予要创建对象的数据成员。但此时如果数据成员中有指针,就会导致两个对象的内部的指针成员指向同意块空间,如果有一个对象被撤销了,那这片空间也就释放了,此时如果撤销另一个对象,就会导致那片空间再次遭到释放,这会带来错误。

    class Student{
    public:
        Student(char *name1, float score1);
        ~Student();
    private:
        char *name;
        float score;
    };
    
    Student stu1("白", 89);
    // Student stu2 = stu1; 这句话会报错
    
  • 深拷贝:将传入对象的值依次赋予要创建对象的数据成员。而对于指针类型变量,为其生成生存空间,随后赋值。

    Student::Student(const Student& stu)
    {
        name = new char[strlen(stu.name) + 1];
        if (name != 0) {
            strcpy(name, stu.name);
            score = stu.score;
        }
    }
    

3. 自引用指针this

this指针保存了当前对象的地址,称为自引用指针。

Score(Score &s);
Score::Score(Score &s){
    if (this == &s)		// 比较地址
        return ;
    this = &s;
}

4. string类

说明:

对于字符串,C语言在string.h头文件中封装了许多方法,用于操作以\0结尾的字符串数组。

而在C++中,除了支持C语言提供的方法外,还提供了个字符串类:string,其封装了对于字符串处理所需的常用操作。

使用前必须引入文件:#include <string>

字符串常用运算符:

=  +=  >  <  >=  <= == !=  [](访问对应下标的字符)  >>(输入)  <<(输出) 

EG:

void stringFunc(){
    string str1 = "abc";
    string str2("123");
    cout<< str1 + str2 <<endl;      // abc123     
    cout<< (str1 == str2) <<endl;   // 0
}

5. 静态成员

5.1 静态数据成员

定义:

在一个类中,如果某个成员被声明为static,则该成员是静态数据成员,不论这个类建立多少个对象,所有对象都共享这一份静态数据成员,从而实现了不同对象之间的数据共享。

特点:

  • 静态数据成员的初始化应在类外单独进行,且应在定义对象之前进行。(因为它要被所有该类的对象共享)
  • 静态数据成员属于类而不是属于单独某一个对象,因此可以通过类名访问(实际上是作用域访问,前提是其被声明为static),即:类名::静态数据成员名
  • 静态数据成员和静态变量一样,在编译时创建并初始化。可以被由该类所创建的所有对象访问,访问方法为:
    • 对象名.静态数据成员名
    • 对象指针->静态成员数据名

5.2 静态成员函数

定义:

在一个类中,如果某个成员函数前有static,那么该成员函数是静态成员函数,不论这个类建立多少个对象,所有对象都共享这一份静态成员函数。且静态成员函数的作用是处理静态数据成员,而不是为了进行数据通信。格式如下:

static 返回类型 函数名 (参数列表);

调用:

三种方式:

类名::静态成员函数名(实参表);
对象名.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);

特点:

  • 一般情况下,静态成员函数不访问非静态成员。如果确实需要,只能通过对象名/对象指针/对象引用进行访问。
  • 私有静态成员不能被类外部的函数和对象访问。
  • 编译系统将静态成员函数定义为内连接,因此假如与当前文件相连接的其它文件中有同名函数,也不会与该静态成员函数发生冲突。

6. 友元

C++提供了私有成员和保护成员,而这些成员只能通过类的成员函数进行访问。但有时我们需要对私有/保护成员频繁进行访问操作,这时多次调用成员函数会带来较大的时间和空间开销,导致程序的效率降低。因此C++提供了友元来对私有/保护成员进行访问。友元包括:

  • 友元函数
  • 友元类

6.1 友元函数

简介:

友元函数有两种:

  • 非成员函数
  • 成员函数

通过友元函数,我们直接对指定类的私有、保护、公有成员进行访问。

6.1.1 非成员函数声明为友元函数

  • 声明:在要访问的目标类内声明,最前面要加上friend关键字。

  • 定义:在类的外部定义。虽然友元函数在目标类内声明,但它并不是成员函数,因此在类外进行定义时并不需要加上类名::

  • EG:

    #include <iostream>
    using namespace std;
    class People{
    private:
    	int id;
    	int age;
    public:
    	People(int id, int age);
        ~People();
        // 友元函数
    	friend int getId(People &p);
    };
    
    People::People(int id, int age)
    {
    	this.id = id;
    	this.age = age;
    }
    // 并不是成员函数,因此定义时无需加上"类名::"进行限定。
    int getId(People &p){
        return p.id;
    }
    
  • 特点:

    • 虽然友元函数在目标类内声明,但它并不是成员函数,因此在类外进行定义时并不需要加上类名::

    • 因为友元函数不是类的成员函数,因此无法直接访问类的数据成员,也无法通过this指针访问,必须通过作为入口参数传递过来的对象名(或对象引用、对象指针)来访问类的数据成员。

    • 一个函数可以同时称为多个类的友元函数。例如:

      #include <iostream>
      using namespace std;
      // 预定义,否则下面友元函数会报错
      class Student;
      
      class People{
      private:
      	int id;
      	int age;
      public:
      	People(int id, int age);
          ~People();
          // 友元函数
      	friend void show_id(People &p, Student &s);
      };
      
      class Student{
      private:
      	int id;
      	int age;
      public:
      	Student(int id, int age);
          ~Student();
          // 友元函数
      	friend void show_id(People &p, Student &s);
      };
      
      
      // 并不是成员函数,因此定义时无需加上"类名::"进行限定。
      void show_id(People &p, Student &s){
          cout << "people's id = " << p.id << endl;
          cout << "student's id = " << s.id << endl;
      }
      

6.1.2 成员函数声明为友元函数

  • 作用:将成员函数声明为友元类,该函数不仅可以访问自己所在类对象中的私有成员,也可以通过friend访问目标类的私有成员。

  • EG:

    #include <iostream>
    using namespace std;
    // 预定义,否则下面友元函数会报错
    class Student;
    
    class People{
    private:
    	int id;
    	int age;
    public:
    	People(int id, int age);
        ~People();
        // 友元函数
    	void show_id(Student &s);
    };
    
    class Student{
    private:
    	int id;
    	int age;
    public:
    	Student(int id, int age);
        ~Student();
        // 友元函数,也是People的成员函数
    	friend void show_id(Student &s);
    };
    
    
    // 是成员函数,因此定义时需要加上"类名::"进行限定。
    void People::show_id(Student &s){
        cout << "people's id = " << this..id << endl;
        cout << "student's id = " << s.id << endl;
    }
    
  • 特点:

    • 因为是成员函数,所以在类外定义时需要加上类名::
    • 因为是成员函数,所以可以通过this指针访问当前对象的成员。

6.2 友元类

可以见一个类声明为另一个类的友元。

class Y{
    ···
};
class X{
    friend Y;    //声明类Y为类X的友元类
};

通过将Y声明为X的友元类,将Y中所有的函数都变为X的友元函数,也就是说,Y中所有的函数都可以访问X都的所有成员。

注意:

友元关系不具有交换性传递性

7 类的嵌套

定义:将一个类作为另一个类的数据成员,就是类的嵌套。

EG:

class Y{
    ···
};
class X{
    Y y;
    ···
};

8 共享数据的保护

8.1 常引用

定义:const 类型& 引用名

int a = 10;
// 常引用,无法对引用进行再次赋值
const int&b = a;
void func(int& m, int& n){}		// 此函数内无法修改m、n的值

8.2 常对象

定义:类名 const 对象名[(参数表)];

const Person p(1, 18);

特点:

  • 常对象在声明时必须赋初值,不能先声明再定义。
  • 常对象只能调用它的常成员函数,不能调用普通成员函数。

8.3 常成员对象

8.3.1 常数据成员

使用const修饰的数据对象,一旦赋值,无法修改。且只能通过成员初始化列表对该数据成员进行赋值

8.3.2 常成员函数

声明:类型 函数名 (参数列表) const;

特点:

  • 在声明和定义时都要加上const,调用时不用。
  • 使用const可以对重载参数进行区分。
  • 既可以访问静态数据成员,也可以访问一般数据成员。
  • 但是不能更新数据成员,也不能调用该类的普通成员函数。这是为了保证常成员函数不对类的数据成员进行修改操作。

EG:

class Date{
private:
	int year;
	int month;
	int day;
public:
	Date(int y, int m, int d) : year(y), month(m), day(d){}
	void showDate();
	void showDate() const;
};

void Date::showDate() {
	//···
}

void Date::showDate() const {
	//···
}

三. 继承与派生

1. 继承案例

#include <iostream>
#include <string>
using namespace std;

class Person{
private:
	string name;
	string id_number;
	int age;
public:
	Person(string name1, string id_number1, int age1) {
		name = name1;
		id_number = id_number1;
		age = age1;
	}
	~Person() {

	}
	void show() {
		cout << "姓名: " << name << "  身份证号: " << id_number << " 年龄: " << age << endl;
	}
};

class Student:public Person{
private:
	int credit;
public:
	Student(string name1, string id_number1, int age1, int credit1):Person(name1, id_number1, credit1) {
		credit = credit1;
	}
	~Student() {

	}
	void show() {
		Person::show();
		cout << "学分: " << credit << endl;
	}
};

int main() {
	Student stu("白", "110103**********23", 12, 123);
	stu.show();

	return 0;
}

2. 基类成员访问规则

派生类可以访问基类成员中除了构造函数和析构函数以外的所有成员,并且可以在派生的过程中对这些成员的访问属性进行修改。从基类继承来成员在派生类中访问属性也不同。

基类中的访问属性 继承方式 在派生类中的访问属性
public public/protected/private public/protect/private(取决于继承方式)
protected public/protected/private protected/protected/private
private public/protected/private 不可直接访问

3. 派生类的构造函数和析构函数

由于派生类无法继承积累的构造函数和析构函数,而我们对派生类进行初始化时,又需要对基类进行初始化,因此必须在派生类的构造函数中完成对基类构造函数所需参数的设置。同理,也必须在派生类的析构函数中完成对基类的销毁。

3.1 构造和析构的顺序

一个案例:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class A{
    public:
    	A() {
    		cout << "A类对象构造中..." << endl;
    	}
    	~A() {
    		cout << "析构A类对象..." << endl;
    	}
    };
    
    class B : public A{
    public:
    	B() {
    		cout << "B类对象构造中..." << endl;
    	}
    	~B(){
    		cout << "析构B类对象..." << endl;
    	}
    };
    
    int main() {
    	B b;
    	return 0;
    }
    
  • 输出:

    A类对象构造中...
    B类对象构造中...
    析构B类对象...
    析构A类对象...
    
  • 分析:可以看出,创建对象时先初始化基类,再初始化派生类。销毁对象时先销毁派生类,再销毁基类。

3.2 构造和析构的规则

基本规则如下:

派生类构造函数的一般格式为:
派生类名(参数总表):基类名(参数表) {
    派生类新增数据成员的初始化语句
}
-----------------------------------------------------------------
含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
    派生类新增成员的初始化语句
}

其顺序为:

  1. 初始化基类
  2. 初始化子对象
  3. 初始化派生类

注意:

  • 如果基类不带参数,则派生类不一定要定义构造函数。
  • 如果派生类的直接基类A也有一个基类,那么派生类的构造函数只需要对直接基类A进行负责,程序会依次向上追溯。

3.3 调整访问属性

所谓修改访问属性,就是基类的成员在派生类中的访问属性(在基类中的访问属性依旧不变)。

修改方式:

把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。

EG:

class B:private A{
private:
    int y;
public:
    B(int x1, int y1) : A(x1) {
        y = y1;
    }
    A::show;               // 访问声明
};

注意:

  • 重写访问属性时,必须加上基类::来说明这时基类中的成员,否则就是在派生类中创建了一个和基类成员同名的成员,会覆盖基类的成员。
  • 访问属性只是在派生类中变了,在基类中依旧不变。
  • 修改访问属性时,无论是基类的函数成员还是数据成员,都去掉类型和参数,只要名字。
  • 对于基类的重载函数名,访问声明将对基类中所有同名函数其起作用。(因为只填了函数的名字)

3.4 多继承

形式:

class 派生类:继承方式1 基类1, 继承方式2 基类2, ... , 继承方式n 基类n{
    派生类新增的数据成员和成员函数
}

默认的继承方式是private

3.5 虚基类

3.5.1 引入

如果一个派生类有多个直接基类,而这些直接基类又有一个共同基类,这就会导致派生类中有多个同名的成员。为了避免产生二义性,派生类访问这些同同名的成员时,必须加上该成员所属的直接基类名,从而唯一的标识一个成员。

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Base{
    protected:
    	int a;
    public:
    	Base(){
    		a = 5;
    		cout << "Base a = " << a << endl;
    	}
    };
    
    class Base1: public Base{
    public:
    	Base1() {
    		a = a + 10;
    		cout << "Base1 a = " << a << endl;
    	}
    };
    
    class Base2: public Base{
    public:
    	Base2() {
    		a = a + 20;
    		cout << "Base2 a = " << a << endl;
    	}
    };
    
    class Derived: public Base1, public Base2{
    public:
    	Derived() {
    		cout << "Base1::a = " << Base1::a << endl;		// 必须要添加直接基类名进行限制,避免产生二义性。
    		cout << "Base2::a = " << Base2::a << endl;
    	}
    };
    
    int main() {
    	Derived obj;
    	return 0;
    }
    
  • 输出:

    Base a = 5
    Base1 a = 15
    Base a = 5
    Base2 a = 25
    Base1::a = 15
    Base2::a = 25
    

3.5.2 虚基类的初始化

3.5.1中,我们虽然通过用直接基类名来限定成员,避免了二义性的产生。但当继承关系较为复杂时,这显然会大大提高编程难度。那么,如果我们能让这些基类中多次继承的成员(也就是上例中的a)只存在一个拷贝(也就是对虚基类只进行一次初始化),那么对于该成员的访问就不会产生二义性了。为此,我们可以通过virtualbase基类声明为虚基类。

语法:

class 派生类:virtual 继承方式 积累名{
    ......
}

对上例进行修改:

#include <iostream>
#include <string>
using namespace std;

class Base{
protected:
	int a;
public:
	Base(){
		a = 5;
		cout << "Base a = " << a << endl;
	}
};

class Base1: virtual public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};

class Base2: virtual public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};

class Derived: public Base1, public Base2{
public:
	Derived() {
		cout << "Base1::a = " << Base1::a << endl;		
		cout << "Base2::a = " << Base2::a << endl;
	}
};

int main() {
	Derived obj;
	return 0;
}

得到运行结果:

Base a = 5
Base1 a = 15
Base2 a = 35
Base1::a = 35
Base2::a = 35

3.5.3 虚基类子类构造函数

如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Base{
    protected:
    	int a;
    public:
    	Base(int a){
    		cout << "Base"<< endl;
    	}
    };
    
    class Base1: virtual public Base{
    private:
        int a_1;
    public:
    	Base1(int a, int a_1): Base(a) {
    		this.a_1 = a_1;
    		cout << "Base1" << endl;
    	}
    };
    
    class Base2: virtual public Base{
    private:
        int a_2;
    public:
    	Base2(int a, int a_2): Base(a) {
    		this.a_2 = a_2;
    		cout << "Base2"<< endl;
    	}
    };
    
    class Derived: public Base1, public Base2{
    private:
        int b;   
    public:
    	Derived(int a, int a_1, int a_2, int b): Base(a),Base1(a_1),Base2(a_2) {
            this.b = a;
    		cout << "Derived" << endl;		
    	}
    };
    
    int main() {
    	Derived obj;
    	return 0;
    }
    
  • 输出:

    Base
    Base1
    Base2
    Derived
    

注意:如果存在一个基类同时继承虚基类和普通基类,那么在调用构造函数时,先调用虚基类的构造函数,再调用普通基类的,最后调用派生类的。

3.6 赋值兼容规则

遵循里氏替换原则,即:所有使用子类的地方,都可以使用父类进行替换。

四. 多态性和虚函数

1. 多态性概述

所谓多态性,就是不同对象接收不同消息时,产生不同的动作。这样就可以用相同的接口访问不同的函数,从而实现一个接口,多种方法

从实现方式上看,多态性分为两种:

  • 编译时多态:
    • 在C++中,编译时多态与连编(把函数名和函数定义连接在一起)有关。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。
    • 优缺点:在程序编译时就知道调用函数的全部信息。因此,这种连编类型的函数调用速度快、效率高,但缺乏灵活性
    • 实现方式:主要通过函数重载运算符重载实现。
  • 运行时多态:
    • C++中通过动态连编实现。动态连编在程序运行时生效,即当程序调用到某一函数名时,才去寻找和连接其程序代码。
    • 优缺点:降低了程序的运行效率,但增强了程序的灵活性。
    • 实现方式:主要通过虚函数实现。

2. 虚函数

定义:

虚函数的声明是在基类中进行的,通过virtual关键字将基类的成员函数声明为虚函数。语法如下:

virtual 返回值 函数名 (形参表){
    函数体;
}

当基类中的某个成员函数被定义为虚函数时,就可以在派生类中对该虚函数进行重新定义,并使用基类调用派生类的实现。

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Person{
    private:
        string flower;
    public:
        Person(string flower = "鲜花"): flower(flower){}
        string getFlower(){
            return this->flower;
        }
        virtual void show(){
            cout<< "人类喜欢:"<< this->flower<< endl;
        }
    };
    
    class Man: public Person{
    public:
        Man(string flower = "油菜花"): Person(flower){}
        // 派生类中的virtual可加可不加
        virtual void show(){
            cout<< "男人喜欢:"<< this->getFlower()<< endl;
        }
    };
    
    class Woman: public Person{
    public:
        Woman(string flower = "西兰花"): Person(flower){}
        void show(){
            cout<< "女人喜欢:"<< this->getFlower()<< endl;
        }
    };
    
    int main(void){
        Person *p = nullptr;
        Person per;
        Man m;
        Woman w;
        per.show();
        p = &m;
        p->show();
        p = &w;
        p->show();
        return 0;
    }
    
  • 输出:

    人类喜欢:鲜花
    男人喜欢:油菜花
    女人喜欢: 西兰花
    

注意:

  • 在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。如不写,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。
  • 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
  • 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
  • 构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。

3. 虚析构函数

问题:

假设现在有一个派生类B,其基类为A,我们建立一个A类型的指针p,再new一个B类型的对象,并令p指向B的地址。那么当使用delete 释放指针p所指向的内存时,系统只会执行基类的析构函数,不执行派生类的析构函数

这是因为当撤销指针p所指向的空间时,采用了静态连编的方式,只执行了基类A的析构函数,而不会执行基类B的析构函数。

解决方法:如果希望程序执行动态连编时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。格式为:

virtual ~类名(){
    函数内容;
}

EG:

  • 代码:

    #include <iostream>
    #include <string>
    using namespace std;
    
    class Base{
    public:
    	virtual ~Base() {
    		cout << "调用基类Base的析构函数..." << endl;
    	}
    };
    
    class Derived: public Base{
    public:
    	~Derived() {
    		cout << "调用派生类Derived的析构函数..." << endl;
    	}
    };
    
    int main() {
    	Base *p;
    	p = new Derived;
    	delete p;
    	return 0;
    }
    
  • 输出:

    调用基类Base的析构函数...
    调用派生类Derived的析构函数...
    

注意:

  • 虽然基类和派生类的析构函数名不一致,但如果将基类的析构函数声明为析构函数,那么其派生类的析构函数也都会变成虚函数
  • 只有在上述情况下,才会出现只调用基类,而不调用派生类析构函数的情况。如果使用的栈空间,或者直接用派生类类型的指针,就不会出现这种问题。

4. 纯虚函数

纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:

virtual 函数类型 函数名(参数表) = 0;

声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。

5. 抽象类

如果一个类至少有一个纯虚函数,那么就称该类为抽象类。对于抽象类的使用有以下几点规定:

  • 因为抽象类中包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类使用,不能家里抽象类对象。
  • 不允许从具体类(不包含纯虚函数的类)中派生出抽象类。
  • 抽象类不能用作函数的返回类型、参数类型或是显式转换的类型
  • 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
  • 如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

6. 实例

问题:

利用C++的多态性,计算三角形、矩形的面积。

代码:

#include <iostream>
#include <string>
using namespace std;

// 定义一个抽象类
class Figure{
// 使用protected,便于派生类直接访问
protected:
    double x;
    double y;
public:
    Figure(double m, double n): x(m), y(n){}
    // 使用需析构函数,避免调用析构函数时只调用基类的
    virtual ~Figure(){
        cout << "Figure is destroyed..."<< endl;
    }
    // 纯虚构函数
    virtual double get_area() = 0;
};

class Triangle: public Figure{
public:
    Triangle(double b, double h): Figure(b, h){}
    ~Triangle(){
        cout<< "Triangle is destroyed..."<< endl;
    }
    double get_area(){
        return x*y/2;
    }
};

class Square: public Figure{
public:
    Square(double b, double h): Figure(b, h){}
    ~Square(){
        cout<< "Square is destroyed..."<< endl;
    }
    double get_area(){
        return x*y;
    }
};

int main(void){
    Figure *p = nullptr;
    p = new Triangle(5, 10);
    cout<< p->get_area()<< endl;
    delete p;
    p = new Square(5, 10);
    cout<< p->get_area()<< endl;
    delete p;
    return 0;
}

输出:

25
Figure is destroyed...
Triangle is destroyed...
50
Figure is destroyed...
Square is destroyed...

五. 运算符重载

1. 概述

如果不进行特殊处理,C++默认的运算符只能对基本的常量或变量进行运算,不能用于对象之间的运算。但有时我们希望对这些运算符功能进行拓展,让他们能够支持更多的运算。运算符重载应运而生。它能够赋予已有的运算符多重含义,使得同一运算符面对不同类型的数据产生不同的效果。

格式如下:

返回值 opetator 运算符 (形参表)
{
    ...
}

包含被重载的运算符的表达式会被编译为对运算符函数的调用,运算符的操作数称为函数的实参,运算的结果就是函数的返回值。运算符可以多次重载。

运算符可以被重载为全局函数,也可以重载为成员函数。一般建议重载为成员函数,这样可以更好的体现运算符和类的关系。下面是一个例子:

  • 代码:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    class Suqare{
    public:
        int _w;
        int _l;
    public:
        Suqare(){}
        Suqare(int w, int l): _w(w), _l(l){}
        ~Suqare(){}
        // 成员函数操作符重载
        Suqare operator + (const Suqare &s);
    };
    Suqare Suqare::operator+(const Suqare &s){
        return Suqare(this->_w + s._w, this->_l + s._l);
    }
    // 定义一个全局操作符重载函数
    Suqare operator - (const Suqare &s1, const Suqare &s2){
        return Suqare(s1._w - s2._w, s1._l - s2._l);
    }
    int main(void){
        Suqare s1(1,2), s2(2,4), s3;
        s3 = s1 + s2;
        cout << s3._w << "," s3._l << << endl;
        s3 = s2 - s1;
        cout << s3._w << "," s3._l << << endl;
        return 0;
    }
    
  • 输出:

    3,6
    1,2
    
  • 结论:从上例可以看出,如果是成员函数进行操作符重载,那么形参数量可以减一,因为可以通过this来获取当前对象本身的属性。

2. 赋值运算符重载

可以对赋值运算符=进行重载,从而对对象进行快速赋值。c++中规定,“=”只能重载为成员函数

EG:

  • 代码:实现字符串的拷贝功能

    #include <iostream>
    #include <cstring>
    using namespace std;
    class String {
    private:
        char * str;
    public:
        String() :str(NULL) { }
        // 最后一个const表示这是一个常函数,避免对变量进行修改。第一个const表示返回的字符串只读,也不许外界修改。
        const char * c_str() const { return str; };
        String & operator = (const char * s);
        ~String();
    };
    String & String::operator = (const char * s)
    //重载"="以使得 obj = "hello"能够成立
    {
        if (str)
            delete[] str;
        if (s) { //s不为NULL才会执行拷贝
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        }
        else
            str = NULL;
        return *this;
    }
    String::~String()
    {
        if (str)
            delete[] str;
    };
    int main()
    {
        String s;
        s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
        cout << s.c_str() << endl;
        // String s2 = "hello!"; //这条语句要是不注释掉就会出错
        s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
        cout << s.c_str() << endl;
        return 0;
    }
    
  • 输出:

    Good Luck,
    Shenzhou 8!
    

3. 深拷贝和浅拷贝

浅拷贝:

当我们使用=,通过一个已经初始化好的对象对另一个对象进行赋值时,对于基础的数据类型,将会采用复制的操作。而对于如数组、指针等指向一片空间的区域,采用的是复制地址而不是复制地址处的内容,这就是浅拷贝。显然,当涉及到内存操作时,浅拷贝可能会出现注入两个指针指向同一空间的情况,这会导致在销毁对象时对同一空间的多次释放,从而引发错误。

深拷贝:

通过重载=赋值操作符,我们可以对默认的赋值方式进行修改,从而实现在对指针类变量赋值时额外再开辟一片内存,并将内容拷贝过去,而不是只拷贝地址。这样就能避免重复释放空间的错误。

4. 运算符重载为友元函数

有时候我们不想对成员函数进行运算符重载,全局函数又无法直接访问类的非共有成员,这时就可以使用友元函数。

EG:

  • 代码:

    #include <iostream>
    using namespace std;
    
    class Complex{
    private:
    	double real, imag;
    public:
    	Complex(double r = 0.0, double i = 0.0): real(r), imag(i) { }
    	friend Complex operator+(Complex& a, Complex& b) {
    		Complex temp;
    		temp.real = a.real + b.real;
    		temp.imag = a.imag + b.imag;
    		return temp;
    	}
    	void display() {
    		cout << real;
    		if (imag > 0) cout << "+";
    		if (imag != 0) cout << imag << "i" << endl;
    	}
    };
    
    int main()
    {
    	Complex a(2.3, 4.6), b(3.6, 2.8), c;
    	a.display();
    	b.display();
    	c = a + b;
    	c.display();
    	c = operator+(a, b);
    	c.display();
    
    	return 0;
    }
    
  • 输出:

    2.3+4.6i
    3.6+2.8i
    5.9+7.4i
    5.9+7.4i
    

5. 运算符重载实现可变长数组

在C++中,我们可以通过通过运算符重载实现一个可变长数组类。它拥有以下特点:

  • 初始化对象是指定数组的元素数量。
  • 可以动态添加数组。
  • 自动完成内存的分配和动态释放问题。
  • 可以正常访问下标。

代码如下:

#include <iostream>
#include <cstring>
using namespace std;
class CArray
{
	int size; //数组元素的个数
	int* ptr; //指向动态分配的数组
public:
	CArray(int s = 0); //s代表数组元素的个数
	CArray(CArray & a);
	~CArray();
	void push_back(int v); //用于在数组尾部添加一个元素 v
	CArray & operator = (const CArray & a); //用于数组对象间的赋值
	int length() const { return size; } //返回数组元素个数
	int & operator[](int i)
	{ //用以支持根据下标访问数组元素,如“a[i]=4;”和“n=a[i];”这样的语句
		return ptr[i];
	};
};
CArray::CArray(int s) : size(s)
{
	if (s == 0)
		ptr = NULL;
	else
		ptr = new int[s];
}
CArray::CArray(CArray & a)
{
	if (!a.ptr) {
		ptr = NULL;
		size = 0;
		return;
	}
	ptr = new int[a.size];
	memcpy(ptr, a.ptr, sizeof(int) * a.size);
	size = a.size;
}
CArray::~CArray()
{
	if (ptr) delete[] ptr;
}
CArray & CArray::operator=(const CArray & a)
{ //赋值号的作用是使 = 左边对象中存放的数组的大小和内容都与右边的对象一样
	if (ptr == a.ptr) //防止 a=a 这样的赋值导致出错
		return *this;
	if (a.ptr == NULL) { //如果a里面的数组是空的
		if (ptr)
		delete[] ptr;
		ptr = NULL;
		size = 0;
		return *this;
	}
	if (size < a.size) { //如果原有空间够大,就不用分配新的空间
		if (ptr)
			delete[] ptr;
		ptr = new int[a.size];
	}
	memcpy(ptr, a.ptr, sizeof(int)*a.size);
	size = a.size;
	return *this;
}
void CArray::push_back(int v)
{ //在数组尾部添加一个元素
	if (ptr) {
		int* tmpPtr = new int[size + 1]; //重新分配空间
		memcpy(tmpPtr, ptr, sizeof(int) * size); //复制原数组内容
		delete[] ptr;
		ptr = tmpPtr;
	}
	else //数组本来是空的
		ptr = new int[1];
	ptr[size++] = v; //加入新的数组元素
}
int main()
{
	CArray a; //开始的数组是空的
	for (int i = 0; i<5; ++i)
		a.push_back(i);
	CArray a2, a3;
	a2 = a;
	for (int i = 0; i<a.length(); ++i)
		cout << a2[i] << " ";
	a2 = a3; //a2 是空的
	for (int i = 0; i<a2.length(); ++i) //a2.length()返回 0
		cout << a2[i] << " ";
	cout << endl;
	a[3] = 100;
	CArray a4(a);
	for (int i = 0; i<a4.length(); ++i)
		cout << a4[i] << " ";
	return 0;
}

输出:

0 1 2 3 4
0 1 2 100 4

其中,[]是双目运算符,a[i]等价于a.operator[](i)

6. C++重载输入输出运算符

6.1 输出<<

在C++中,左移运算符<<被和cout一起用作输出,左移运算符本身并没有这种效果,之所以能做到这一步,是因为它被重载了。

coutostream的对象,它们都是在头文件<iostream>中被声明的。ostream将<<进行了重载,并且为了适应多种类型的输出,将其重载了多次,例如:

  • 输出字符串:

    ostream & ostream::operator << (const char *s)
    {
        // 输出s的代码
        return *this;
    }
    
  • 输出整形:

    ostream & ostream::operator << (int n)
    {
    	// 输出整形n的代码
    	return *this;
    }
    

重载函数的返回类型是ostream的引用,依旧是ostream类型,因此我们可以链式调用重载函数,例如:

cout << "this is a string" << 5;
// 等价于
(cout.operator<<("this is a string")).operator<<(5);

6.2 输入>>

和输出类似,右移运算符>>istream类中被多次重载,并通过istream的对象cin进行调用,这些也都是定义在头文件<iostream>中的。一个重载案例是:

istream & istream::operator << (const char *s)
{
    // 输入s的代码
    return *this;
}

6.3 案例

问题:

现在有一个Complex类,假定 c 是 Complex 复数类的对象,现在希望写cout<<c;就能以 a+bi 的形式输出 c 的值;写cin>>c;就能从键盘接受 a+bi 形式的输入,并且使得 c.real = a, c.imag = b。

分析:

显然,需要对ostream和istream进行重载,但是这两个类在标准头文件中,无法修改,因此只能使用全局函数来进行运算符重载。此外,如果使用全局函数,想要访问到Complex的数据成员,需要将其声明为友元函数。

代码:

#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex
{
	double real,imag;
public:
	Complex( double r=0, double i=0):real(r),imag(i){ };
	friend ostream & operator<<( ostream & os,const Complex & c);
	friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
	os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
	return os;
}
istream & operator>>( istream & is,Complex & c)
{
	string s;
	is >> s; //将"a+bi"作为字符串读入, "a+bi" 中间不能有空格
	int pos = s.find("+",0);
	string sTmp = s.substr(0,pos); //分离出代表实部的字符串
	c.real = atof(sTmp.c_str());//atof库函数能将const char*指针指向的内容转换成 float
	sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
	c.imag = atof(sTmp.c_str());
	return is;
}
int main()
{
	Complex c;
	int n;
	cin >> c >> n;
	cout << c << "," << n;
	return 0;
}

输出:

13.2+133i 87
13.2+133i,87

7. 强制类型转换重载

在C++中,类的名字本身也是一种运算符,也就是类型强制转换运算符。需要注意的是:

  • 类型强制运算符是单目运算符,只能被重载为成员函数,不能重载为全局函数。

EG:

  • 代码:

    #include <iostream>
    using namespace std;
    class Complex
    {
        double real, imag;
    public:
        Complex(double r = 0, double i = 0) :real(r), imag(i) {};
        operator double() { return real; } //重载强制类型转换运算符 double
    };
    int main()
    {
        Complex c(1.2, 3.4);
        cout << (double)c << endl; //输出 1.2
        double n = 2 + c; //等价于 double n = 2 + c. operator double()
        cout << n; //输出 3.2
    }
    
  • 输出:

    1.2
    3.2
    

8. C++重载++和--运算符

自增运算符++、自减运算符--都可以被重载,但是它们有前置、后置之分。

++为例,假设 obj 是一个 CDemo 类的对象,++objobj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:、

CDemo & CDemo::operator ++ ()
{
    //...
    return * this;
}

那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。

为了解决这个问题,C++ 规定,在重载++或--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:

#include <iostream>
using namespace std;
class CDemo {
private:
	int n;
public:
	CDemo(int i=0):n(i) { }
	CDemo & operator++(); //用于前置形式
	CDemo operator++( int ); //用于后置形式
	operator int ( ) { return n; }
	friend CDemo & operator--(CDemo & );
	friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
	n ++;
	return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
	CDemo tmp(*this); //记录修改前的对象
	n++;
	return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d)
{//前置--
	d.n--;
	return d;
}
CDemo operator--(CDemo & d,int)
{//后置--
	CDemo tmp(d);
	d.n --;
	return tmp;
}
int main()
{
	CDemo d(5);
	cout << (d++ ) << ","; //等价于 d.operator++(0);
	cout << d << ",";
	cout << (++d) << ","; //等价于 d.operator++();
	cout << d << endl;
	cout << (d-- ) << ","; //等价于 operator-(d,0);
	cout << d << ",";
	cout << (--d) << ","; //等价于 operator-(d);
	cout << d << endl;
	return 0;
}

输出:

5,6,7,7
7,6,5,5

六. 函数模板和类模板

1. 引入

在编写函数和类时,有时会出现这样的情况,具体实现方式完全一致,但因此参数类型、返回值类型、数据类型等因素的不同,导致不得不写多个函数或者类(因为C++是强类型语言,无法隐式转换,且有些类型本来就无法转换),这样无疑会增加操作难度。那么,如何简单的完成这一任务呢?

define是一个办法,例如:

#define MAX(x,y) ((x>y) ? x : y);

这样就避免了因数据类型不同而重复编写代码。

但是,define可能导致代码在不该替换的地方进行了替换,从而造成错误,因此C++中不主张使用宏定义。

所有一个更好的办法就是使用模板。

2. 函数模板

声明格式:有两种方式:

  •   template <typename 类型参数>
      返回类型 函数名(模板形参表)
      {
          函数体
      }
    
  •   template <class 类型参数>
      返回类型 函数名(模板形参表)
      {
          函数体
      }
    

可以定义多个类型参数:

  •   template <typename 类型参数1, typename 类型参数2,>
      返回类型 函数名(模板形参表)
      {
          函数体
      }
    

EG:

  • 问题:求当前数组中最大的值。

  • 代码:

    #include <iostream>
    using namespace std;
    
    template <typename T>			// template <class T> 也可以
    T Max(T *array, int size = 0) {
    	T max = array[0];
    	for (int i = 1	; i < size; i++) {
    		if (array[i] > max) max = array[i];
    	}
    	return max;
    }
    
    int main() {
    	int array_int[] = {783, 78, 234, 34, 90, 1};
    	double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
    	int imax = Max(array_int, 6);
    	double dmax = Max(array_double, 6);
    	cout << "整型数组的最大值是:" << imax << endl;
    	cout << "双精度型数组的最大值是:" << dmax << endl;
    	return 0;
    }
    

注意:

  • template语句和函数语句中不允许插入别的语句。
  • 在函数模板中允许使用多个类型参数。但是,应当注意template定义部分的每个类型参数前必须有关键字typenameclass
  • 函数模板也可以重载。
  • 函数模板与同名的非模板函数可以重载。在这种情况下,调用的顺序是:
    1. 寻找一个完全匹配的非模板函数。
    2. 如果找不到,再找函数模板。

3. 类模板

建立类模板,主要是因为有时候,类的数据类型、成员函数的返回类型、形参类型不确定,因此使用类模板。语法与函数模板相似。下面是一个案例:

template <typename T>
class Three{
private:
    T x, y, z;
public:
    Three(T a, T b, T c) {
        x = a; y = b; z = c;
    }
    T sum() {
        return x + y + z;
    }
    T mul();
}
// 如果在类外定义类模板的成员函数,需要加上模板声明,且函数名前要加上"类名<类型参数>::"
template <typename T>
T Three<T>::(){
	return x * y * z;
}

EG:

  • 代码:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    const int CAPACITY = 10;
    
    template <typename T>
    class Stack{
    private:
        T stack[CAPACITY];      // 这里使用数组,简化初始化操作
        int top;
    public:
        void init();
        void push(T t);
        T pop();
    };
    
    // 必须加上<T>定义,否则无法识别类
    template <typename T>
    void Stack<T>::init(){
        top = 0;
    }
    
    template <typename T>
    void Stack<T>::push(T t){
        if(this->top == CAPACITY){
            cout << "stack is full"<< endl;
            return;
        }
        stack[top++] = t;
    }
    
    template <typename T>
    T Stack<T>::pop(){
        if (top == 0){
            cout << "stack is empty" << endl;
            return 0;
        }
        return stack[--top];
    }
    
    int main(void){
        // 注意:在初始化模板类时需要指定参数类型
        Stack<int> s;
        s.init();
        s.push(3);
        s.push(5);
        cout << s.pop() << endl;
        cout << s.pop() << endl;
        cout << s.pop() << endl;
        return 0;
    }
    
  • 输出:

    5
    3
    stack is empty
    0
    

注意:

  • 只要是模板类的类外声明,在类名后面都必须加上<类型参数>
  • 使用模板类时,要指定该模板类的参数类型。

七. 输入和输出

C++支持C的输入、输出结构,同时也建立了一套面向对象的输入、输出结构,且更加安全。其输入/输出通过字节流的形式实现。

1. 输入/输出头文件

  • iostream:包含了对输入/输出流进行操作所需的基本信息。其中包含了istreamostream类,并在其中对移位运算符>><<进行重载,用户可以通过已经头文件中已经创建好的cincout对象进行读写操作。
  • fstream:用于用户管理文件的IO操作。
  • strstream:用于字符串流的IO操作。通过它使用字符串流对象对内存字符串空间进行IO。
  • iomanip:用于控制输入/输出的格式。头文件汇中创建了setwfixed等对象。

2. 输入/输出流类

类名 说明 头文件名
ios 抽象流基类 iostream
通用输入流类和其它输入流类 istream iostream
输入文件流类 ifstream fstream
输入字符串流类 istrstream strstream
通用输出流类和其它输出流类 istream iostream
输出文件流类 ofstream fstream
输出字符串流类 ostrstream strstream
通用输入输出流类和其它输入/输出流类的基类 iostream iostream
输入/输出文件流类 fstream fstream
输入/输出字符串流类 strstream strstream

3. 输入/输出预定义对象

在C++提供的输入/输出流库中,有一些预定的流对象,用于流操作。

EG:

  • 标准输入流对象:cin
  • 标准输出流对象:cout
  • 非缓冲型的标准出错流对象:cerr
  • 缓冲型标准出错流对象:clog

4. 常用的输入/输出流成员函数

在使用istreamostream的对象时,常使用到相关成员函数:

  • put():用于输出

    cout/put(单字符/字符型变量/ASCII码);
    
  • get():用于输入。它可以用来接收空白符,而>>运算符默认无法接收空白符。

    cin.get(字符型变量);
    
  • getline():用于输入,可以使用数组或指针

    cin.getline(字符数组, 字符个数, 终止标志字符);
    cin.getline(字符指针, 字符个数, 终止标志字符);
    
  • ignore():用于输入。作用是跳过输入流中n个字符(默认是1个),在遇到指定的终止符时(默认是EOF)提前结束。

    cin.ignore(n,终止字符);
    

5. 输入/输出格式化

C++中,依旧可以使用printf()scanf()函数对输入/输出进行格式化,并在此基础上提供了两种新的格式控制方式:

  • 使用ios类中有关格式控制的流成员函数进行格式控制。
  • 使用称为操作符的特殊类型的函数进行格式控制。

此处省略常用函数。

6. 文件操作

6.1 操作步骤

对文件继续读写的步骤如下:

  1. 建立流文件。
  2. 打开目标文件,没有就新建一个。
  3. 进行读/写操作。
  4. 关闭流文件。

文件流类:

类名 说明 功能
istream 输入文件流类 用于文件的输入
ofstream 输出文件流类 用于文件的输出
fstream 输入/输出文件流类 用于文件的输入/输出

6.2 打开/关闭流文件

打开文件:有两种方式:

  • 三种文件流类的构造函数都会自动打开文件。

  • 使用成员函数opean()打开文件,函数原型如下:

    void open(char const *,int filemode,int =filebuf::openprot);
    
    • 第一个是要打开的文件迷宫

    • 第二个是文件打开方式,可以取以下值:

      image-20231129111533269

    • 第三个是文件保护方式,一般使用默认值。

关闭文件:使用close()成员函数。