C++学习笔记

1. C++ 基础概念

1.1 输入输出

1.1 输出语句:

cout<< 表达式1 << 表达式2 << …… << 表达式n;

  • 例1:

    cout<<"hello world"<<endl;
    //endl 表示换行
    
  • 例2:

    cout<<a,b,c;		//错误,不能依次输入多项
    cout<<a<<b<<c;		//正确
    cout<<a+b+c;		//正确
    

1.2 输入语句:

cin>> 变量1 >> 变量2 >> …… >>变量n;

  • 例1:

    cin>>a>>b>>c;
    

1.3 在标准输入流和输出流中使用控制符

**注:**使用前引入头文件

< iostream >, < iomanip >

控制符作用
dec设置数值的技术为10
hex设置数值的基数为16
oct设置数值的基数为8
setfill(c)设置填充字符c,c可以是字符常量或字符变量
setprecision(n)设置浮点数的精度为n位。
在以一般十进制小数形式输出时,n代表有效数字的位数
在以fixed(固定小数位数)形式和scientific(指数)形式输出是,n位小数位数
setw(n)设置字段宽度为n位
fixed设置浮点数以固定的小数位数显示(与**setprecision**搭配使用)
scientific设置浮点数以科学计数法(即指数形式)显示
left输出数据左对齐
right输出数据右对齐
skipws忽略签到的空格
uppercase数据以十六进制形式输出是字母以大写表示
lowercase数据以十六进制形式输出是字母以小写表示
shoupos输出证书是给出“+”号
  • 例子:

    double a = 123.456789012345
    cout<<a;						//输出:123.456
    cout<<setprecision(9)<<a		//输出:123.456789
    cout<<setprecision(6);			//回复默认格式(精度为6,即有效数字为6)
    cout<<fixed;					//输出:123.456789
    cout<<fixed<<setprecision(8)<<a;//输出123.45678901
    cout<<scientific<<a;			//输出:1.2345678e+02
    cout<<scientific<<setprecision(4)<<a;//输出:1.2346e+02
    
  • 正数输出的例子:

    int b = 123456;
    cout<<b;
    cout<<hex<<b;						//输出:1e240
    cout<<uppercase<<b;					//输出:1E240
    cout<<setw(10)<<b<<','<<b;			//输出:123456,123456
    cout<<setfill('*')<<setw(10)<<b;	//输出:****123456
    cout<<showpos<<b;					//输出:+123456
    

1.2 引用

1.引用的概念

​ 引用就是某一变量的一个别名,它是某个已存在变量的另一个名字。对引用的操作与对变量名直接操作完全一样

2.引用的初始化

类型标识符 &引用名 = 目标变量名;

​ 上述格式中,“&”并不是取地址操作符,而是起标识作用,标识所定义的标识符是一个引用。引用声明完成以后相当于目标变量有两个名称

例:

int a = 10;
int& b = a;

3.定义引用注意事项

  • 引用在定义是必须初始化,如 int &b; 是错误的,并未进行初始化

    int a=10;
    int &b=a;//正确
    int &b;	 //error
    
  • 引用在初始化时只能绑定变量不能绑定常量值

    int &p=2;//error
    
  • 引用一旦初始化其值不能再更改,否则其值会改变

    int a=10;
    int b=20;
    int &p=a;
    p=b;
    cout<<a<<endl;
    return 0;
    
    //输出结果
    20
    
  • 数组不能定义引用,因为数组时一组数据,无法定义其别名

4.引用与指针的区别

  • 不存再空引用,引用必须连接到一块合法的内存
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象
  • 引用必须在创建时被初始化,指针可以在任何时间被初始化

5.引用应用

例:交换数字

#include<iostream>
using namespace std;

void swap(int& x, int& y)
{
    int temp=x;
    x=y;
    y=temp;
}

int main()
{
    int a,b;
    cin>>a>>b;
    swap(a,b);
    cout<<a<<" "<<b<<endl;
    return 0;
}

//输入
1 2
//输出
2 1

2. 类

2.1 类的声明和对象的定义

2.1.1 类和对象的关系

  • C++中对象的类型称为

  • 类是对象的抽象,而对象是类的具体实例

  • C++中如同定义结构体一样,先声明一个类类型,然后用它定义若干个同类型的对象。对象就是类型的一个变量

2.1.2 声明类类型

格式:(与声明结构体类似)

class Student           //以class开头,类名为Student
{
    int num;
    char name[20];
    char sex;               //以上3行是数据成员
    void display()          //这是成员函数
    {
        cout<<"num:"<<num<<endl;
        cout<<"name:"<<name<<endl; 
        cout<<"sex:"<<sex<<endl;            //以上3行是函数中的操作语句
    }
};

Student stud1,stud2;            //定义了两个Student类的对象

名称:

  • **类头:**第一行,由关键字class与类名Student组成
  • **类体:**花括号括起来的
  • **成员表:**类中的所有成员 (把数据和操作封装在一起)

2.1.3 成员访问限定符:

  • private:被声明为私有的成员,只能被本类中的成语那函数引用,类外函数不能调用(友元类除外)
  • public:被声明为公用的成员,既可以被本类中的成员函数所引用,也可以被类的作用域内的其他函数引用
  • protected:用protected声明的成员称为受保护的成员,它不能被类外访问,但可以被派生类的成员函数访问

格式:

class 类名
{
    public:
    	公有的数据和成员函数;
    private:
    	私有的数据和成员函数;
};

2.1.4 定义对象的方法

1. 先声明类类型,然后再定义对象
Student stu1,stu2;		//Student是已经声明的类类型
class Student stud1,stud2;	//与上句等效,这是C语言特点,此句虽在C++中仍适用,但上句更简便
2. 在声明类的同时定义对象
class Student                                              //声明类类型
{
public:                                                    //先声明公用部分
    void display()
    {
        cout<<"num:"<<num<<endl;
        cout<<"name:"<<name<<endl;
        cout<<"sex:"<<sex<<endl;
    }
private:                                                   //后声明私有部分
    int num;
    char name[20];
    char sex;
}stu1,stu2;                                                //定义两个Student类的对象
3. 不出现类名,直接定义对象
class
{
    private:
    
    public:
}stu1,stu2;

直接定义对象在C++中是合法的、允许的,但不提倡使用

2.1.5 重载成员函数

重载概念:

  • 编程中重载是指函数相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。
  • 重载是可使相同函数、运算符等处理不同类型数据或接受不同个数的参数的一种方法。
  • 如果过同一作用域内的几个函数名字相同单行参列表不同,称之为重载函数

由于类名是成员函数名的一部分,所以一个类的成员函数与另一个类的成员函数即使同名,也不能认为是重载

例子:

class student
{
public:
    float grade();
    float grade(float newGPA);
protected:
    ……
};

class Slope
{
public:
    float grade();
protected:
    ……
}t;

char grade(float value);

int main()
{
    Student s;
    slope t;
    s.grade(3.2);             //对应 Student::grade(float);
    float v = s.grade;        //对应 Student::grade();
    char c  = grade(v);       //对应 grade();
    float m = t.grade();      //对应 Slope::grade();
}

在主函数运行时,一共调用了4个grade() 函数

2.2 类的成员函数

2.2.1 成员函数的性质

  • 它是属于一个类的成员,出现在类体中。它可以被指定为private、public或protected
  • 私有的成员函数只能被本类中的其他成员函数所调用
  • 成员函数可以访问本类对象中任何成员(包括私有的和公用的),可以引用在本作用域中有效的数据

2.2.2 在类外定义成员函数

  • 例子:

    class Student
    {
    public:
        void display();                    //公用成员函数原型声明
    private:
        int num;
        string name;
        char sex;                          //以上3行是私有数据成员
    };
    
    void Student::display()                //在类外定义display类函数
    {
        cout<<"num:"<<num<<endl;           //函数体
        cout<<"name:"<<name<<endl;
        cout<<"sex:"<<sex<<endl;
    }
    Student stud1,stud2;                   //定义两个类对象
    
  • 注意:

    • 在类体重定义函数,不需要再函数名前面加上类名
    • 成员函数在类外定义时,必须在函数名前面加上类名,予以限定,“::”是作用域限制符作用域运算符,用它声明函数是属于哪个类
    • 如果在作用域运算符::的前面没有类名,或者函数名前面既无类名又无作用域运算符,如::display() 或 display ,则表示display函数不属于任何类,这个函数不是成员函数,而是全局函数
    • 类函数必须先在类体中作原型声明,然后再类外定义,否则编译时会出错。即类体的位置应在函数定义之前
    • 在类的内部对成员函数做声明,在类体外定义成员函数,这是程序设计的一种良好习惯

2.2.3 内置成员函数

  • 为了减少时间开销,如果在类体中定义的成员函数中不包括循环等控制结构,C++系统自动的对他们作为内置函数来处理

  • 一般的内置函数要用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已被隐含地指定为内置函数

  • 如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置函数,调用这些成员函数的过程和调用一般函数是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。

    class Student
    {
    public:
        inline void display();                          //声明此成员函数为内置函数
    private:
        int num;
        string name;
        char sex;
    };
    
    inline Student::display()                       //在类外定义display函数为内置函数
    {
        cout<<"num:"<<num<<endl;
        cout<<"name:"<<name<<<endl;
        cout<<"sex:"<<sex<<endl;
    }
    
    

2.3 调用成员函数

1、调用一个成员函数

调用成员函数的形式类似于访问一个结构对象的分量,先指明对象,再指明分量。它必须指定对象和成员名,否则无意义

例:

class Tdate
{
public:
    void Set(int ,int , int );
    int IsLeapYear();
    void Print();
private:
    int month;
    int day;
    int year;
};

void func()					//普通函数
{
    Tdate oneday;			//创建对象
    oneday.Set(2,19,1998);	//调用其成员函数
    oneday.Print();
}

2、用指针调用成员函数

例:

#include<iostream>
using namespace std;

class Tdate
{
public:
    void Set(int ,int , int );
    int IsLeapYear();
    void Print();
private:
    int month;
    int day;
    int year;
};

void someFunc(Tdate* pS)
{
    pS -> Print();                      //pS是s对象的指针
    if(pS -> IsLeapYear())
        cout<<"oh oh\n";
    else
        cout<<"right\n";
}

int main()
{
    Tdate s;
    s.Set(2,15,1998);
    someFunc(&s);                   //对象的地址传给指针
}

3、用引用传递来访问成员函数

例:

#include<iostream>
using namespace std;

class Tdate
{
public:
    void Set(int ,int , int );
    int IsLeapYear();
    void Print();
private:
    int month;
    int day;
    int year;
};

void someFunc(Tdate& refs)
{
    refs.Print();                       //refs是s对象的别名
    if(refs.IsLeapYear())
        cout<<"oh oh\n";
    else
        cout<<"right\n";
}

int main()
{
    Tdate s;
    s.Set(2,15,1998);
    someFunc(s);                    //对象的地址传给引用
}

4.在成员函数中访问成员

​ 成员函数必须用对象来调用。

例:

#include<iostream>
using namespace std;

class Tdate
{
public:
    void Set(int ,int , int );
    int IsLeapYear();
    void Print();
private:
    int month;
    int day;
    int year;
};

void Tdate::Set(int m , int d , int y)
{
    month = m ;//不能在month前加对象名
    s.month = m;//error
    day = d;
    year  = y;
}

void Tdate::Print()
{
    cout<<month<<"/"<<day<<"/"<<year<<endl;
}

int Tdate::IsLeapYear()
{
    return 0;
}

int main()
{
    Tdate s;
    Tdate t;
    s.Set(2,15,1998);
    t.Set(3,15,1997);
    s.Print();
    t.Print();
}

2.4 名字识别

1、类的作用域

  • 一个类的所有成员位于这个类的作用域内,一个类的任何成员都能访问同一类的任一其他成员
  • C++认为一个类的全部成员都是一个整体的相关部分
  • 类作用域是指类定义和相应的成员函数定义范围,在范围内,一个类的成员函数对用一类的数据成员具有无限制的访问权
  • 在类作用域外,对一个类的数据成员和成员函数的访问受到访问权限的制约

2、可见性

两个名字在同一作用域内,由于层次不同,内层名字往往会遮挡外层名字。

​ 例如,m是X类的数据成员,且其成员函数定义中有同名的局部作用域变量,则数据成员m会被隐藏

class X
{
public:
    void f1();
    void f2();
protected:
    int m;
}x;

void X::f1()
{
    m=5;
}

void X::f2()
{
    int m;
    m=2;		//X::m 被隐藏
}

​ 类X的数据成员m的作用域尽管在类X中,但是成员函数中定义了同名的局部作用域变量后,就把数据成员m给隐藏了

3、类名遮挡

类名允许与其他变量名或函数名相同。

​ 当类名与程序中的其他变量或者函数名相同时,可以通过下面的方法来正确访问

(1)如果一个非类型名隐藏了类型名,则类型名通过加前缀可用

​ 例,一个类名被在函数中的形参所覆盖,在该函数内,要定义一个类对象,则加上class即可

class Sample
{
    //...
};

void func(int Sample)	//函数形参隐藏了原类名
{
    class Sample s;
    Sample++;
}

(2)如果一个类型名隐藏了非类型名,则用一般作用域规则即可

例如:

#include<iostream>
using namespace std;

int s=0;			//全局变量
void func()
{
    class s
    {
        int b;
    };				//类s隐藏了全局变量s
    s a;			//定义一个类对象
    ::s = 3;		//引用全局变量,要加入::
}					//class s 作用域结束
int g = s;			//用全局变量给变量g初始化

int main()
{
    func();
    cout<<s;
    return 0;
}
//输出结果
3

在函数中定义的类称为局部类,局部类的作用域在定义该类的函数块中

局部类的成员函数必须在类定义内部定义。如果在类外部包含该类的函数内部中定义,则导致在函数内部定义函数的矛盾,如果在包含类的函数外部定义,则局部类无法与其取得联系

4、名空间

定义:名空间是指某名字在其中必须唯一的作用域

C++规定,一个名字不能同时指两种类型,如

class C	//定义一个类类型
{
    //..
};
int C;	//error:定义了一个不同类型但同名

非类型名(变量名、常量名、函数名、对象名或枚举名)不能重名,如

Student a;	//定义一个对象
void a();	//error:函数名与对象名同名

**类型与非类型不在同一名空间。**在一个作用域中,一个名字可以声明为一个类型,也可以声明为一个非类型。当二者同时登场时,类型名要加前缀,以区别非类型名。如:

class stat			//先定义类类型
{
    //..
};
stat a;				//定义类对象
void stat(stat* p);	//函数名与类名相同,不在同一名空间
class stat b;		//必须区分stat是类型还是函数
stat(0);			//非类型名:函数调用

3. 使用类与对象

3.1 构造函数

3.1.1 用构造函数实现数据成员的初始化

定义:构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行

特点:

  • 构造函数的名字必须与类名同名,而不能任意命名,以便编译系统能识别它并把它作为构造函数处理
  • 他不具有任何类型,不返回任何值

例:

#include<iostream>
using namespace std;
class Time                   //声明Time类
public:                      //以下为公用函数
    Time()                   //定义构造成员函数,函数名与类名相同
    {
        hour=0;              //利用构造函数对对象中的数据成员赋初值
        minute=0;
        sec=0;
    }
    void set_time();         //成员函数声明
    void show_time();        //成员函数声明
private:                     //以下为私有数据
    int hour;
    int minute;
    int sec;    
};

//也可以
//在类中声明 Time();
//在类外定义构造函数
//Time::Time()
//{...}

void Time::set_time()        //定义成员函数,向函数成员赋值
{
    cin>>hour;
    cin>>minute;
    cin>>sec;
}

void Time::show_time()       //定义成员函数,输出数据成员的值
{
    cout<<hour<<" : "<<minute<<" : "<<sec<<endl;
}

int main()
{
    Time t1;                        
    t1.set_time();
    t1.show_time();
    
    Time t2;
    t2.set_time();
    t2.show_time();
    return 0;
}
//输入
10 25 54
//输出
10 : 25 : 54
0 : 0 : 0

**分析:**在类中定义了构造函数Time,它和所在的类同名,在建立对象时会自动指向构造函数,根据构造函数Time的定义,起作用是对该对象中的全部数据成员赋予初值0。

说明:

  1. 构造函数没有返回值,因此也没有类型,他的作用只是对对象进行初始化。因此也不需要在定义构造函数时声明类型直接写成Time(); ,不能写成void Time()

  2. 构造函数不需要用户调用,也不能被用户调用

    t1.Time(); //这时错误的用法

    构造函数是在定义对象时有系统自动执行的,而且只能执行一次。

  3. 构造函数一般声明为public

  4. 可以用一个类类对象初始化另一个类对象,如

    Time t1;		//建立对象t1,同时调用构造函数t1.Time();
    Time t2 = t1;	//建立对象t2,并用一个t1初始化t2
    

    此时,把对象t1的个数据成员的值赋值到t2相应各成员,而不调用构造函数t2.Time()

  5. 如果用户没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作

3.1.2 带参数的构造函数

**定义:**可以从外面讲不同的数据传递给构造函数,以实现不同的初始化的构造函数

**一般形式: ** 构造函数名(类型1 形参1,类型2 形参2,...)

定义对象的一般形式: 类名 对象名(实参1,实参2,...)

在建立对象时把实参的值传递给构造函数相应的形参,把他们作为数据成员的初值

**例:**有两个长方柱,其高、宽、长分别为(1)12,20,25;(2)10,14,20。求他们的体积

#include<iostream>
using namespace std;

class Box                          //声明Box类
{
public:
    Box(int ,int ,int);            //声明带参数的构造函数
    int volume();                  //声明计算体积的函数
private:
    int height;
    int width;
    int length;
};

Box::Box(int h , int w , int len)   //类外定义带参数的构造函数
{
    height = h;
    width = w;
    length = len;
}

int Box::volume()                       //定义计算体积的函数
{
    return(height*width*length);
}

int main()
{
    Box box1(12,25,30);		//定义对象的语句形式
    cout<<"The volume of box1 is "<<box1.volume()<<endl;

    Box box2(15,30,21);
    cout<<"The volume of box2 is "<<box2.volume()<<endl;
    return 0;
}

//输出结果
The volume of box1 is 9000
The volume of box2 is 9450

3.1.3 用参数初始化表对数据成员初始化

**定义:**C++还提供另一种初始化数据成员的方法——参数初始化表

一般形式:

类名::构造函数名([参数表])[:成员初始化表]
{
    [构造函数体]
}
//方括号内为可有可无

例:

Box::Box(int h,int w,int len):height(h),width(w),length(len){}

说明:

即在原来函数首部的末尾加一个冒号,然后列出参数的初始化表。

上面的初始化表表示:用形参h的值初始化数据成员height,用形参w的初始化数据成员width,用形参len的初始化数据成员length。后面的花括号是空的,即函数体是空的。没有任何执行语句

注:

如果数据成员是数组,则应当在构造函数的函数体中用语句对其赋值,而不能在参数初始化表中对其初始化,如

#include<iostream>
#include<string.h>

using namespace std;

class Student
{
public:
    Student(int n , char  s , char nam[]):num(n),sex(s)
    {
        strcpy(name , nam);
    }
    void show();
private:
    int num;
    char sex;
    char name[20];
};

void Student::show()
{
    cout<<num<<" "<<sex<<" "<<name<<endl;
}

int main()
{
    Student stu1(10101 , 'm' , "Wang_li");
    stu1.show();
    return 0;
}

3.1.4 构造函数的重载

定义:在一个类中可以定义多个构造函数,以便为对象提供不同的初始化的方法供用户选用。这些构造函数具 有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载

例:

#include<iostream>
using namespace std;

class Box
{
public:
    Box();
    Box(int h, int w , int len):height(h),width(w),length(len){}
    int volume();
private:
    int height;
    int width;
    int length;
};

Box::Box()
{
    height = 10;
    width = 10;
    length = 10;
}

int Box::volume()
{
    return (height*width*length);
}

int main()
{
    Box box1;
    cout<<box1.volume()<<endl;
    Box box2(15,30,25);
    cout<<box2.volume()<<endl;
    return 0;
}

//输出结果
1000
11250

3.1.5 使用默认参数的构造函数

构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,即如果用户不指定实参值,编译系统就是形参取默认值

例:

#include<iostream>
using namespace std;

class Box
{
public:
    Box(int h=10 , int w=10 , int len=10);
    int volume();
private:
    int height;
    int width;
    int length;
};

Box::Box(int h,int w,int len)
{
    height = h;
    width = w;
    length = len;
}

int Box::volume()
{
    return (height*width*length);
}

int main()
{
    Box box1;
    cout<<box1.volume()<<endl;
    Box box2(15);
    cout<<box2.volume()<<endl;
    Box box3(15,30);
    cout<<box3.volume()<<endl;
    Box box4(15,30,20);
    cout<<box4.volume()<<endl;
    return 0;
}

//输出结果
1000
1500
4500
9000

3.2 析构函数

定义: 析构函数的作用与构造函数相反,它的名字是类名的前面加一个“~”符号。(在C++中“ ~ ”是位取反运算符)

当对象的生命期结束时,会自动执行析构函数。

具体的说如果出现以下4种情况,程序就会执行析构函数:

  1. 如果在一个函数中定义了一个对象(假设时自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数
  2. 静态局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数
  3. 如果定义了一个全局的对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数)时,调用该全局的对象的析构函数
  4. 如果用new运算符动态的建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数

析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用

程序设计者实现设计好析构函数,以完成所需的功能,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作

析构函数不返回任何值,没有函数类型,也没有函数参数。由于没有函数参数,因此它不能被重载

一个类可以有多个构造函数,但是只能由一个析构函数

例:

#include<string>
#include<iostream>
using namespace std;
class Student
{
public:
    Student(int n,string nam , char s)
    {
        num=n;
        name=nam;
        sex=s;
        cout<<"Constructor called"<<endl;
    }
    ~Student()								//定义析构函数
    {
        cout<<"Destructor called"<<endl;	//输出有关信息
    }
    void display()
    {
        cout<<num<<endl;
        cout<<name<<endl;
        cout<<sex<<endl<<endl;
    }
private:
    int num;
    string name;
    char sex;
};

int main()
{
    Student stu1(10010,"Wang_li",'f');
    stu1.display();
    Student stu2(10011,"Zhang_fang",'m');
    stu2.display();
    return 0;
}

//输出结果
Constructor called
10010
Wang_li
f

Constructor called
10011
Zhang_fang
m

Destructor called		//执行stud2的析构函数
Destructor called		//执行stud1的析构函数
    

**//**在本例中,析构函数并无任何实质上的作用,只是输出一个信息

3.3 调用构造函数和析构函数的顺序

先构造的后析构,后构造的先析构,相当于一个栈,先进后出

下面归纳一下系统在什么时候调用构造函数和析构函数:

  • 如果在全局范围中定义对象(即在所有函数之外定义的对象),那么他的构造函数在本文件模块中的所有函数(包括main函数)执行之前调用。当main函数执行完毕或调用exit函数值,调用析构函数

  • 如果定义的是局部自动对象(如在函数中定义对象),则在建立对象是调用其构造函数。如果对象所在的函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束时,对象释放时先调用析构函数

  • 如果在函数中定义静态局部对象,则只在程序第一次调用此函数定义对象时调用一次构造函数,在调用函数结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数

3.4 对象数组

Student stu[3] = 
{
    Student(1001,18,87);
    Student(1002,19,76);
    Student(1003,18,72);
}

例:

#include<iostream>
using namespace std;
class Box
{
public:
    Box(int h=10 , int w=12 , int len=15):height(h),width(w),length(l){}
    int volume;
private:
    int height;
    int width;
    int length;
};

int Box::volume()
{
    return(height*length*width);
}

int main()
{
    Box a[3] = 
    {
        Box(10,12,15),
        Box(15,18,20),
        Box(16,20,26)
    };
    cout<<a[0].volume()<<endl;
}

3.5 对象指针

3.5.1 指向对象的指针

对象空间的起始地址就是对象的指针

定义指向类对象的指针变量的一般形式为

类名 * 对象指针名

//如果有一个类
class Time
{
public:
    int hour;
    int minute;
    int sec;
    void get_time();
};
//在此基础上有以下语句:
Time* p1;
Time t1;
p1 = t1;
//这样p1就是指向Time类对象的指针变量,指向t1

可以通过对象指针访问对象和对象的成员,如

*p1				//p1指向的对象,即t1
(* p1).hour		//p1所指向的对象中的hour成员。即t1.hour
p1->hour		//p1所指向的对象中的hour成员
(*pt).get_time	//p1所指向的对象中的成员函数
p1->get_time

3.5.2 指向对象成员的指针

1. 指向对象数据成员的指针
  • 定义指向对象数据成员的指针变量名的一般形式

    数据类型名 * 指针变量名

  • 定义指向对象数据成员的指针变量的方法和定义指向普通变量的指针变量方法相同

    int* p1;
    
  • 如果Time类的数据成员hour为公用的整型数据,则可以在类外通过指向对象数据成员的指针变量访问对象数据成员hour

    p1 = &t1.hour;
    cout<<*p1<<endl;
    
2. 指向对象成员函数的指针

注意:定义指向对象成员函数的指针变量的方法定义指向普通函数的指针变量的方法有所不同

  • 这里重温指向普通函数的指针变量的定义方法

    格式:*类型名(指针变量名)(参数列表);

    void fun();
    
    void(*p)();	 //p是指向void型函数的指针变量
    p=fun;		//将fun函数的地址赋给指针变量p,p就是指向函数fun
    (*p)();		//调用fun函数
    
  • 定义对象成员函数的方法:

    *数据类型名(类名 :: 指针变量名)(参数列表);

    void(Time::*p2)();	//定义p2为指向Time类中公用成员函数的指针变量
    p2=&Time::get_time;	//函数名后不用加括号()
    //将Time类中的get_time成员函数的地址复制给p2
    

    例题:

    用不同的方法输出时间记录器的时、分、秒

    #include<iostream>
    using namespace std;
    class Time
    {
    public:
        Time(int , int , int);
        int hour;
        int minute;
        int sec;
        void Get_time();
    };
    
    Time::Time(int h , int m , int s)
    {
        hour = h;
        minute = m;
        sec = s;
    }
    
    void Time::Get_time()
    {
        cout<<hour<<" : "<<minute<<" : "<<sec<<endl;
    }
    
    int main()
    {
        Time t1(10,13,56);
        int* p1 = &t1.hour;
        cout<<*p1<<endl;
        t1.Get_time();
        Time* p2 = &t1;
        p2->Get_time();
        void(Time:: *p3)();
        p3=&Time::Get_time;
        (t1.*p3)();
    }
    

3.5.3 this指针

在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this

它是指向本类对象的指针,他的值是当前被调用的成员函数所在的对象的起始地址

例如,当调用成员函数 a.volume 时,编译系统就把对象a的起始地址赋给this指针,于是在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。

如volume函数要计算height * width * length 的值,实际是执行:

(this->height)*(this->width)*(this->length)
//相当于执行
 (a.height)*(a.width)*(a.length)
//上述两个句子的括号是不必要的,只是为了方便阅读

this指针是隐式使用的,它是作为参数被传递给成员函数的

3.6 共用数据的保护

3.6.1 常对象

可以在定义对象时加关键字const,指定对象为常对象

常对象必须有初值,如

Time const t1(12,34,46);

定义常对象的一般形式为

类名 const 对象名 [(实参表)]

const 类名 对象名 [(实参表)]

二者等价。在定义常对象时,必须同时对峙初始化,之后不能再改变

注意:

  • 如果一个对象被声明为常对象,则通过该对象只能调用它的常成员函数,而不能调用该对象的普通成员函数(除了有系统自动调用的隐式的构造函数和析构函数)。常成员函数是常对象唯一的对外接口

  • 常成员函数可以访问常对象中的数据成员,但任然不允许修改常对象中数据成员的值

  • 若一定要修改常对象中的某个数据成员的值,可以对该数据成员声明为mutable,如

    mutable int count;
    

3.6.2 常对象成员

1. 常数据成员

常数据成员的值是不能改变的。

**注意:**只能通过构造函数的参数初始化表对常数据成员进行初始化,任何其他函数都不能对常数据成员赋值

Time::Time(int h):hour(h){}		//通过参数初始化表对长数据成员hour进行初始化

构造函数只能用参数初始化表对长数据成员进行初始化

2. 常成员函数

声明常成员函数的一般形式为:

类型名 函数名(参数表)const

如,

void get_time()const;	//注意const的位置在函数名和括号之后

const是函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加

数据成员非const的普通成员函数const成员函数
非const的普通数据成员可以引用,也可以改变值可以引用,但不可以改变值
const数据成员可以引用,但不可以改变值可以引用,但不可以改变值
const对象不允许可以引用,但不可以改变值

3.6.3 指向对象的常指针

将指针变量声明为const型,这样指针变量始终保持为初值,不能改变,即指向不变

定义指向对象的长指针变量的一般形式为

类名 * const 指针变量名;

Time t1(10,12,15),t2;	//定义对象
Time* const ptr;		//const位置在指针变量名前面,指定ptr是常指针变量
ptr=&t1;				//ptr指向对象t1,此后不能再改变指向
ptr=&t1;				//error错误,ptr不能改变指向

//也可合并成
Time* const ptr=&t1;

3.6.4 指向常对象的指针变量

1.指向常变量

定义指向常变量的指针变量的一般形式为:

const 类型名 * 指针变量名

const char* ptr;

说明:

  • 如果一个变量已被声明为常变量,只能用指向常变量的指针变量指向它

    const char c[]="boy";	//定义const型的char数组
    const char* p1;			//定义p1为指向const型的char变量的指针元素
    p1=c;					//合法
    char* p2 =c;			//error不合法
    
  • 指向常变量的指针变量除了可以指向常变量,还可以指向未被声明为const的变量。此时不能通过此指针变量改变该变量的值,如

    char c1='a';		//定义字符变量c1,它并未声明为const
    const char* p;		//定义一个指向常变量的指针变量
    p = &c1;
    *p='b';				//error非法,不能通过p来改变变量c1的值
    c1='b';				//合法,没有通过p访问c1,c1不是常变量
    

    只是指针变量访问c1期间,c1具有常变量的特征,其值不能改变。在其他情况下,c1任然是一个普通的变量,其值可以该改变

  • 如果函数的形参是指向普通变量的指针变量,实参只能用指向普通变量的指针,而不能用指向const变量的指针

2.指向常对象

指向常对象的指针变量的概念和使用与上述类似,只要将“变量”换成“对象”即可

  • 如果一个对象已被声明为常对象,只能用指向常对象的指针变量指向它,而不能用一般的指针变量

  • 如果定义了一个指向常对象的指针变量,并使它指向一个非const的对象,则其指向的对象是不能通过该指针变量来改变的

  • 指向常对象的指针最常用于函数的形参,目的是在保护形参指针所指向的对象,使它在函数执行过程中不被修改

    int main()
    {
        void fun(const Time*);
        Time t1(10,13,56);
        fun(&t1);
        return 0;
    }
    
    void fun(const Time* p)
    {
        p->hour=18;		//error错误,不能改变其值
        cout<<p->hour<<endl;
    }
    

    **注意:**当希望在调用函数时对象的值不被修改,就应当把形参定义为指向常对象的指针变量,同时用对象的地址作实参(对象可以是const或非const型)

  • 如果定义了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值的,但是指针变量本身的值是可以改变的,如

    const Time* p =&t1;	//定义指向常对象的指针变量p,并指向对象t1
    p = &t2;		//p改为指向t2,合法
    

3.6.5 对象的常引用

例:

#include<iostream>
using namespace std;

class Time
{
public:
    Time(int ,int ,int);
    int hour;
    int minute;
    int sec;
};

Time::Time(int h,int m,int s)
{
    hour=h;
    minute=m;
    sec=s;
}

void fun(Time &t)		//如果不希望在函数中修改实参t1的值,可以把fun函数的形参t声明为const
{						//即,void fun(const Time &t);
    t.hour=18;
}

int main()
{
    Time t1(10,13,56);
    fun(t1);
    cout<<t1.hour<<endl;
    return 0;
}

//输出结果
18

3.6.6 const型数据小结

形式含义
Time const t;t是常对象,其值在任何情况下都不能改变
void Time::fun()const;fun是Time类中的常成员函数,可以引用,但不能修改本类中的数据成员
Time* const p;p是指向Time类对象的常指针变量,p的指向不能改变
const Time* p;p是指向Time类对象的常指针变量,p指向的类对象的值不能通过p来改变,p的指向可以改变
const Time &t1 = t;t1是Time类对象t的引用,二者指向同一存储空间,t的值不能改变

3.7 对象的动态建立和释放

new

Box* p1;
p1 = new Box;

delete

delete p1;

3.8 对象的赋值和复制

3.8.1 对象的赋值

如果对一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,或者说,一个对象的值可以赋给另一个同类的对象。

对象名 1 = 对象名 2

Student stu1,stu2;	//定义两个同类的对象
stu1 = stu2;		//将stu1赋给stu2

注意:

类的数据成员中不能包括动态分配的数据,否则在赋值时,可能出现严重后果

3.8.2 对象的复制

  • 一般形式:

    类名 对象2(对象1);

    类名 对象2 = 对象1;//与上者等效

    Box box2(box1);	//用对象1复制出对象2
    Box box2 = box1;	//效果相同
    
  • 复制构造函数 (拷贝构造函数)

    一般格式:

    类名::函数名(类名& 参数)

    例:

    Box::Box(Box& b)
    {
        height=b.height;
        width=b.width;
        length=b.length;
    }
    
    int main()
    {
        Box box2(box1);	//将box1的值赋值给box2
    }
    
  • 复制与复制的区别:

    对象的赋值时对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。

    对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同

3.8.3 浅拷贝、深拷贝

  • 浅拷贝

    简单地制作一个该对象的拷贝,而不对它本身进行资源分配和复制,就得面临一个麻烦的局面:两个对象都拥有同一个资源,当对象析构时,该资源将经历两次资源返还。

    例:

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    class Person
    {
        char* pName;
    public:
        Person(char* pN);
        ~Person();
    };
    
    Person::Person(char* pN)
    {
        cout<<"Constructing "<<pN<<endl;
        pName = new char[strlen(pN)+1];
        if(pName!=0)
            strcpy(pName,pN);
    }
    
    Person::~Person()
    {
        cout<<"Destructing "<<pName<<endl;
        pName[0] = '\0';
        delete pName;
    }
    
    int main()
    {
        Person p1("Randy");
        Person p2=p1;
    }
    
    
    //输出结果
    Constructing Randy
    Destructing Randy
    Destructing ␦u
    然后是程序崩溃
    

    ​ 程序开始运行时,创建p1对象,p1对象的构造函数从堆中分配空间并赋给数据成员pName,同时产生第一行输出;

    ​ 执行第二行“Person p2=p1;”时,因为没有定义拷贝构造函数,于是就调用默认拷贝构造函数,使得p2和p1完全一样,并没有分配新的对空间给p2;如图

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    ​ 主函数结束时,对象逐个析构,先构造的后析构,后构造的先析构。析构p2时,将堆中字符串清成空串,然后将堆空间返还给系统,并同时得到第二行输出;

    ​ 析构p1时,因为这时pName指向的是空串,所以第三行输出中显示的只是Destructing;

    ​ 当执行“delete pName;”时,系统报错,显示第四行结果

    创建p2时,对象p1被复制给了p2,但资源并未复制,因此p1和p2指向同一个资源,这称为浅拷贝

  • 深拷贝

    例,下面的代码实在上面程序的基础上,增加一个Person类的拷贝构造函数

    #include<iostream>
    #include<cstring>
    using namespace std;
    
    class Person
    {
    protected:
        char* pName;
    public:
        Person(char* pN);
        Person(Person& p);
        ~Person();
    };
    
    Person::Person(char* pN)
    {
        cout<<"Constructing "<<pN<<endl;
        pName = new char[strlen(pN)+1];
        if(pName!=0)
            strcpy(pName,pN);
    }
    
    Person::Person(Person& p)
    {
        cout<<"Copying "<<p.pName<<" into its own block\n";
        pName = new char[strlen(p.pName)+1];
        if(pName!=0)
            strcpy(pName,p.pName);
    }
    
    Person::~Person()
    {
        cout<<"Destructing "<<pName<<endl;
        pName[0] = '\0';
        delete pName;
    }
    
    int main()
    {
        Person p1("Randy");
        Person p2=p1;
    }
    
    //输出结果
    Constructing Randy
    Copying Randy into its own block
    Destructing Randy
    Destructing Randy
    

    ​ 程序开始运行时,创建p1对象,产生第一行输出;

    ​ 用p1去创建p2对象,调用自己定义的拷贝构造函数,于是得到第二行输出;

    ​ 拷贝构造函数中,不但复制了对象空间,也复制堆内存空间,见图

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    ​ 当主函数退出时,先后析构p2和p1,但这时候对象们有其各自的资源,所以析构函数能够进行,产生最后两行输出

    ​ 创建p2时,对象p1被复制给了p2,同时资源也作了复制,因此p1和p2指向不同的资源,这称为深拷贝

3.9 静态成员

3.9.1 静态数据成员

静态数据成员的一种特殊的数据成员,它以关键字static,例如

class Box
{
public:
  int volume();
private:
    static int height;
    int width;
    int length;
};
  • 如果希望各对象中的数据成员的值是一样的,就可以把它定义为静态数据成员,这样它就为个对象所共有,而不只属于某个对象的成员,所有对象都可以引用它。
  • 静态对象的数据成员在内存中只占用一份空间,而不是每个对象都分别为他保留一份空间
  • 静态成员的值对所有对象都是一样的,如果改变它的值,则在个对象中这个数据成员的值都同时改变了。
  • 可以节约空间,提高效率

说明:

  • 如果只声明了类而为定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间,但是静态数据成员不属于一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。静态数据成员时在所有对象之外单独开辟空间。只要在类中指定了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用

  • 它不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员时在对象建立时分配空间,在对象撤销时释放)。静态数据成员是在程序开始运行时被分配空间,到程序结束时才释放空间

  • 静态数据成员可以初始化,但是只能在类外进行初始化,如

    int Box::height=10;
    

    一般形式:数据类型 类名::静态数据成员名=初值

    只要在类体中声明静态数据成员时加static,不必在类外初始化语句中加static

    注意:不能用参数初始化表对静态数据成员初始化,如

    Box(int h,int w,int len):height(h);	//error,height是静态数据成员
    
  • 静态数据成员既可以通过对象名引用,也可以通过类名来引用

    #include<iostream>
    using namespace std;
    
    class Box
    {
    public:
      Box(int ,int);
      int volume();
      static int height;
      int width;
      int length;
    };
    Box::Box(int w,int len)
    {
        width = w;
        length = len;
    }
    
    int Box::height = 10;
    int main()
    {
        Box a(15,20),b(20,30);		//建立两个对象
        cout<<a.height<<endl;		//通过对象名a引用静态数据成员height
        cout<<b.height<<endl;		//通过对象名b引用静态数据成员height
        cout<<Box::height<<endl;	//通过类名引用静态数据成员height
        return 0;
    }
    
    //输出结果
    10
    10
    10    
    

    注意:如果静态数据成员被定义为私有的,则不能在类外直接引用,而必须通过公用的成员函数引用

  • 有了静态数据成员,各对象之间的数据有了沟通的渠道,实现了数据共享,因此可以不适用全局变量

9.9.2 静态成员函数

定义静态成员函数

static int volume

静态成员函数是类的一部分而不是对象的一部分,如果要在类外调用公用的静态成员函数,要用类名和域运算符“::”,如

Box::volume();

静态成员函数主要用来访问静态数据成员,而不访问非静态成员

//假如在一个静态成员函数中有以下语句
cout<<height<<endl;		//若height已声明为static,则引用本类中的静态成员,合法
cout<<width<<endl;		//若width是非静态数据成员,不合法,error

如果一定要引用本类的非静态成员,应该加对象名和成员运算符,如

cout<<a.width<<endl;	//引用本类对象a中的非静态成员

注:因为静态成员对象并不属于某一对象,所以没有this指针。所以不能进行默认访问

**例:**统计学生平均成绩。使用静态成员函数

#include<iostream>
using namespace std;

class Student
{
public:
    Student(int n,int a,float s):num(n),age(a),score(s) {}
    void total();
    static float average();
private:
    int num;
    int age;
    float score;
    static float sum;
    static int count;
};

void Student::total()
{
    sum+=score;
    count++;
}

float Student::average()
{
    return (sum/count);
}

float Student::sum=0;
int Student::count=0;

int main()
{
    Student stud[3]=
    {
        Student(1001,18,70),
        Student(1002,19,78),
        Student(1005,20,98)
    };
    int n;
    cin>>n;
    for(int i=0; i<n ; i++)
        stud[i].total();
    cout<<n<<endl<<Student::average()<<endl;
    return 0;
}

//输入
3
//输出
3
82    

最好养成习惯:只用静态成员函数引用静态数据成员,而不引用非静态数据成员

3.12 构造函数用于类型转换

class Student
{
    public:
    Student(char*);
};

void fn(Student& s);

int main()
{
    fn("Jenny");
}

​ 因为有Student(char*)的构造函数,,又有fn(Student& s)函数,于是,fn(“Jenny”)便被认为是fn(Student(“Jenny”)),最终予以匹配。

​ 把构造函数用来从一种类型转换为另一种类型,这是C++从类机制中获得的附加性能

​ 但要注意:

  • 只会尝试含有一个参数的构造函数

  • 如果有二义性,则放弃尝试,如

    void addCourse(Student& s);
    void addCourse(Teachar& t);
    
    int main()
    {
        addCourse("prof");//error,存在二义性
    }
    

3.10 友元

3.10.1 友元函数

如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在类体中用friend对其进行声明,此函数就称为本类的友元函数。

友元函数可以访问这个类中的私有成员,

1.将普通函数声明为友元函数
#include<iostream>
using namespace std;
class Time
{
public:
    Time(int ,int ,int);
    friend void display(Time &);        //声明display函数为Time类的友元函数
private:
    int hour;
    int minute;
    int sec;
};
Time::Time(int h,int m,int s)
{
    hour=h;
    minute=m;
    sec=s;
}
void display(Time& t)
{
    cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl;
}
int main()
{
    Time t1(10,13,56);
    display(t1);
    return 0;
}

//输出结果
10:13:56

disp函数中,在引用私有私有数据成员时,必须加上对象名,要写成

cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl;

因为display函数不是Time类的成员函数,没有this指针,不能默认引用Time类的数据成员,必须指定要访问的对象

2.友元成员函数

friend函数不仅可以使一般函数,也可以是另一个类中的成员函数

#include<iostream>
using namespace std;
class Date;
class Time
{
public:
    Time(int ,int ,int);
    void display(Date &);
private:
    int hour;
    int minute;
    int sec;
};

class Date
{
public:
    Date(int ,int ,int);
    friend void Time::display(Date &);		//声明Time中的display函数为本类的友元成员函数
private:
    int month;
    int day;
    int year;
};

Time::Time(int h,int m,int s)
{
    hour=h;
    minute=m;
    sec=s;
}

void Time::display(Date &d)
{
    cout<<d.month<<"/"<<d.day<<"/"<<d.year<<endl;
    cout<<hour<<":"<<minute<<":"<<sec<<endl;
}

Date::Date(int m,int d,int y)
{
    month=m;
    day=d;
    year=y;
}

int main()
{
    Time t1(10,13,56);
    Date d1(12,25,2004);
    t1.display(d1);
    return 0;
}

//输出结果
12/25/2004
10:13:56

注意:

  • 在函数名display的前面要加display所在的对象名
  • display成员函数的实参是Date类对象d1,否则就不能访问对象d1 中的私有数据
  • 在Time::display的函数中引用Date类私有数据时必须加上对象名,如d.month
  • 要先声明Date类名,表示此类将在稍后声明。只包含类名,不包括类体。否则使用不了友元成员函数

9.10.2 友元类

不仅可以将一个函数声明为一个类的“朋友”,而且可以将一个类(如B类)声明为另一个类(A类)的“朋友”

这时B类就是A类的友元类

友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员

在A类的定义体中用以下的语句声明B类为其友元类

friend B

声明友元类的一般形式为:
friend 类名

说明:

  • 友元的关系是单向的,而不是双向的。如果声明了B类是A类的友元类,不等于A类是B类的友元类,A类中的成员函数不能访问B类中的私有数据,B类中的成员函数能访问A类中的私有数据
  • 友元的关系不能传递,如果B类是A类的友元类,C类是B类的友元类,不等于C类是A类的友元类。如果想让C类是A类的友元类,应在A类中另外声明
  • 一般情况下并不把整个类声明为友元类,而只将确实有需要的成员函数声明为友元函数,这样更安全一点

3.11 类模板

template<class numtype>
class Compare
{
public:
    Compare(numtype a,numtype b)
    {x=a;y=b;}
    numtype max()
    {
        return(x>y)?x:y;
    }
    numtype min()
    {
        return(x<y)?x:y;
    }
private:
    numtype x,y;
};

说明:

  • 声明类模板时要增加一行

    template<class 类型参数名>

    template是声明类模板时必须写的关键字。在template后面的尖括号内的内容为模板的参数表,关键字class表示其后面的是类型参数。

    在本例中,numtype是一个类型参数名,这个名字是任意取的,只要是合法的标识符即可。

  • 原有的类型名 int 换成虚拟类型参数名:numtype。这样就能实现一类多用

由于类模板包含类型参数,因此又称为参数化的类

类模板是类的抽象,类是类模板的实例。

类模板的使用:

Compare是类模板名,而不是一个具体的类,类模板中的类型numtype并不是一个实际的类型,只是一个虚拟的类型,无法用它去定义对象,必须用实际类型名去替代虚拟的类型具体做法是:

Compare < int > cmp (4,7);

即在类模板名之后再尖括号内指定实际的类型名,在进行编译时,编译系统就用int取代类模板中的类型参数numtype,这样就把类模板具体化了

其一般形式为:

类模板名<实际类型名> 对象名(参数表);

例:声明一个类模板,利用它分别实现两个整数、浮点数和字符的比较,求出大数和小数

#include<iostream>
using namespace std;
template<class numtype>
class Compare
{
public:
    Compare(numtype a,numtype b)
    {
        x=a;
        y=b;
    }
    numtype max()
    {
        return(x>y)?x:y;
    }
    numtype min()
    {
        return(x<y)?x:y;
    }
private:
    numtype x,y;
};

int main()
{
    Compare<int> cmp1(3,7);
    cout<<cmp1.max()<<endl;
    cout<<cmp1.min()<<endl<<endl;

    Compare<float> cmp2(45.78,93.6);
    cout<<cmp2.max()<<endl;
    cout<<cmp2.min()<<endl<<endl;

    Compare<char> cmp3('a','A');
    cout<<cmp3.max()<<endl;
    cout<<cmp3.min()<<endl;

    return 0;
}

//输出结果
7
3

93.6
45.78

a
A

上面列出的类模板中的成员函数实在类模板内定义的,如果改为在类模板外定义,不能用一般定义类成员函数的形式,应当写成类模板的形式:

template<class numtype>
numtype Compare<numtype>::max()
{
    return(x>y)?x:y;
}

归纳以上的介绍,可以这样声明和使用类模板:

  • 先写出一个实际的类

  • 将此类中准备改变的类型(如int要改为float或char)改用一个自己指定的虚拟类型名(如上例中的numtype)

  • 在类声明前面加入一行

    template<class 虚拟类型名>

    template<class numtype>
    class Compare
    {};
    
  • 用类型模板定义对象时用以下形式

    类型模板名<实际类型名>对象名;

    类型模板名<实际类型名>对象名(实参表);

    Compare<int>cmp;
    Compare<int>cmp(3,7);
    
  • 如果在类模板外定义成员函数,应写成类模板形式:

    template<class 虚拟类型参数>

    函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表){}

    说明:

    1. 类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如

      template<class T1,class T2>

      class someclass

      {……};

      在定义对象时分别代入实际的类型名,如

      someclass<int,double> obj;

    2. 和使用类一样,使用类模板使要注意其作用于,只能在其有效作用域内用它定义对象。

      如果类模板是在A文件开头定义的,则A文件范围内为有效作用域,但不能在B文件中用类模板定义对象

    3. 模板可以有层次,一个类模板可以作为基类,派生出派生模板类

4. 运算符重载

10.1运算符重载的方法

运算符重载的方法是定义一个重载运算符的函数,使指定的运算符不仅能实现原有的功能,而且能实现在函数中指定的新的功能。

在使用重载的运算符时,系统就自动调用该函数,以实现相应的功能。

运算符重载的实质是函数的重载


重载运算符的函数一般格式如下:

函数类型 operator 运算符名称(形参表)

{对运算符的重载处理}

例如,将“+”用于Complex类(复数)的加法运算,函数的圆形可以是:

Complex operator + (Complex& c1,Complex& c2);

operator是关键字,是专门用于定义重载运算符的函数

运算符名称就是C++已有的运算符

注意:函数名是由 operator 和运算符组成的,上面的 “operator +” 就是函数名


例:对运算符“+”实行重载,使之能用于两个复数相加

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(){real=0; imag=0;}
    Complex(double r, double i){real=r;imag=i;}
    Complex operator + (Complex& c2);
    void display();
private:
    double real;
    double imag;
};

Complex Complex::operator+(Complex& c2)
{
    Complex c;
    c.real = real+c2.real;
    c.imag = imag+c2.imag;
    return c;
}

void Complex::display()
{
    cout<<"("<<real<<","<<imag<<"i)"<<endl;
}

int main()
{
    Complex c1(3,4) , c2(5,-10),c3;
    c3=c1+c2;
    cout<<"c1=";c1.display();
    cout<<"c2=";c2.display();
    cout<<"c1+c2=";c3.display();
    return 0;
}

上面的运算符重载函数还可以写的更简练一点

Complex Comoplex::operator + (Complex& c2)
{return Complex(real+c2.real , imag+c2.imag);}

return语句中 Complex(real+c2.real , imag+c2.imag) 是建立一个临时对象,它没有对象名,只是一个无名对象。在建立临时对象过程中调用构造函数。return 语句将此临时对象作为函数返回值

在例题中如何实现一个常量和一个复数对象相加?

c3=Complex(3,0)+c2;

10.2 重载运算符的规则

  1. C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载

    例如有人想将 “**” 定义为幂运算符,这是不行的

  2. C++允许重载运算符

    C++允许重载的运算符:
    双目算术运算符+,-,*,/,%
    关系运算符==,!=,<,>,<=,>=
    逻辑运算符||,&&,!
    单目运算符+(正),-(负),*(指针),&(取地址)
    自增自减运算符++,- -
    位运算符|(按位或),&(按位与),~(按位取反),^(按位异或),<<(左移),>>(右移)
    赋值运算符=,+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=
    空间申请与释放new,delete,new[ ],delete[ ]
    其他运算符()(函数调用),->(成员访问),->*(成员指针访问),,(逗号),[ ](下标)
    不能重载的运算符(只有5个)
    成员访问运算符.
    成员指针访问运算符*
    域运算符::
    长度运算符sizeof
    条件运算符?:

    前两个运算符不能重载是为了保护访问成员的功能不被改变

    域运算符和sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征

  3. 重载不能改变运算符运算对象的个数

    如关系运算符 “>” 是双目运算符,重载后仍为双目运算符,需要两个参数

    运算符 “+ , - , * , & ”等既可以作为单目运算符,也可以作为双目运算符,可以分别将他们重载为单目运算符或双目运算符

  4. 重载不能改变运算符的优先级别

    例如 * 和 / 优先于 + 和 -,不论怎样进行重载,各运算符之间的优先级别不会改变

  5. 重载不能改变运算符的结合性

    如赋值运算符 “ = ”是右结合性(自右向左),重载后仍为右结合性

  6. 重载运算符的函数不能有默认的参数

    否则改变了运算符参数的个数,与前面第3点矛盾

  7. 重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象(或类对象的引用)

    也就是说,参数不能全部是C++的标准类型,以防止用户修改用于标准类型数据的运算符的性质,如下面

    int operator + (int a,int b)
    {return(a-b);}
    //error
    
  8. 用于类对象的运算符一般必须重载,但有两个例外,=,&不必用户重载

    “=” 可以用于每一个类对象,可以利用它在同类对象之间相互赋值

    “&” 地址运算符也不必重载,它能返回类对象在内存中的起始地址

  9. 从理论上说,可以将一个运算符重载为执行任意操作

10.3 运算符重载函数作为类成员函数和友元函数

对运算符重载的函数有两种处理方式:

  1. 把运算符重载的函数作为类的成员函数

  2. 运算符重载函数在类外定义(不是类的成员函数),在类中把它声明为友元函数

当重载函数是类中的成员函数时,有一个参数是隐含的,运算符函数使用 this 指针隐式地访问类对象的成员,如this->real+c2.real。所以”+”是双目运算符,而类中重载函数中只有一个参数

例:将运算符 “+” 重载为适用于复数加法,重载函数不作为成员函数,而放在类外,作为Complex类的友元函数

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex()
    {
        real=0;
        imag=0;
    }
    Complex(double r,double i)
    {
        real=r;
        imag=i;
    }
    friend Complex operator + (Complex& c1,Complex& c2);
    void display();
private:
    double real;
    double imag;
};

Complex operator + (Complex& c1,Complex& c2)
{
    return Complex(c1.real+c2.real,c1.imag+c2.imag);
}

void Complex::display()
{
    cout <<"(" <<real <<" ," <<imag <<"i)" <<endl;
}

int main()
{
    Complex c1(3,4),c2(5,-10),c3;
    c3=c1+c2;
    cout <<"c1=" ;
    c1.display( );
    cout <<"c2 =" ;
    c2. display( );
    cout <<"c1 + c2 =" ;
    c3. display( );

    return 0;
}

如果将运算符重载函数作为成员函数,它可以通过this指针自由地访问本类的数据成员,因此可以少写一个函数的参数

  • 但必须要求运算表达式(如c1+c2)中第1个参数是一个类对象,而且与运算符函数的类型相同。因为必须通过类的对象去调用该类的成员函数,而且只有运算符重载函数返回值与该对象同类型,运算结果才有意义。

如果将双目运算符重载为友元函数时,由于友元函数不是该类的成员函数,因此在函数的形参表列中必须有两个参数,不能省略

注意:数学上的交换律在此不适用,

c3=i+c2;
c3=c2+i;	//error 类型不匹配

如果希望适用交换律,则应再重载一次运算符“+”,如

Complex operator + (Complex& c,int& i)
{return Complex(i+c.real,c.imag);}

默认规定:

  1. 赋值运算符“=”,下表运算符“[ ]”、函数调用运算符“()”、成员运算符“->”必须作为成员函数重载
  2. 流插入“<<” 和 流提取运算符“>>”、类型转换运算符函数不能定义为类的成员函数,只能作为友元函数
  3. 一般将单目运算符和复合运算符重载为成员函数
  4. 一般将双目运算符重载为友元函数

10.4 重载双目运算符

例:声明一个字符串类String,用来存放不定长的字符串,重载运算符“==”,“>”和“<”,用于两个字符串的等于、小于和大于的比较运算

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

class String
{
public:
    String(){p=NULL;};
    String(char* str);
    friend bool operator > (String& s1,String& s2);
    friend bool operator < (String& s1,String& s2);
    friend bool operator == (String& s1,String& s2);
    void display();
private:
    char* p;
};

String::String(char* str)
{
    p=str;
}

void String::display()
{
    cout<<p;
}

bool operator > (String& s1,String& s2)
{
    if(strcmp(s1.p,s2.p)>0)
        return true;
    else
        return false;
}

bool operator < (String& s1,String& s2)
{
    if(strcmp(s1.p,s2.p)<0)
        return true;
    else
        return false;
}

bool operator == (String& s1,String& s2)
{
    if(strcmp(s1.p,s2.p)==0)
        return true;
    else
        return false;
}

void compare(String& s1,String& s2)
{
    if(operator > (s1,s2)==1)
    {
        s1.display();
        cout<<">";
        s2.display();
    }
    else if(operator < (s1,s2)==1)
    {
        s1.display();
        cout<<"<";
        s2.display();
    }
    else if(operator == (s1,s2)==1)
    {
        s1.display();
        cout<<"=";
        s2.display();
    }
    cout<<endl;
}

int main()
{
    String s1("Hello"),s2("Book"),s3("Computer"),s4("Hello");
    compare(s1,s2);
    compare(s2,s3);
    compare(s1,s4);

    return 0;
}

//输出结果
Hello>Book
Book<Computer
Hello=Hello

10.5 重载单目运算符

​ 单目运算符只有一个操作数,如!a,-b,&c,*p,++i,- -i

​ 由于单目运算符只有一个操作数,所以运算符重载函数只有一个参数,如果运算符重载函数函数作为成员函数还可以省略此参数

++自增、- -自减运算符氛围前置和后置,C++中区分两者的方式是:在自增(自减)运算符重载函数中,增加一个int型形参,就是后置自增(自减)运算符函数

Time operator ++();		//前置自增
Time operator ++(int);	//后置自增

(注:后置自增多了一个int型的参数,增加这个参数只是为了与前置自增运算符重载函数有所区别,此外没有任何作用,在定义函数时也不必使用此参数,因此可省写参数名,只需在括号中写int即可)

实现自增函数时直接返回此类的指针,如

Time Time::operator ++()
{
    if(++sec>=60)
    {
        sec-=60;
        ++minute;
    }
    return *this;
}

在实现后置自增函数时需要先保存未自增时的数据,函数结束后再返回此数据,如

Time Time::operator ++(int)
{
    Time temp(* this);
    sec++;
    if(sec>=60)
    {
        sec-=60;
        ++minute;
    }
    return temp;
}
//此时temp类就是原本未自增时的数值

例:有一个TIme类,包含数据成员minute、sec,模拟秒表,每秒走1秒,满60秒进1分钟,秒钟重置。要求输出分和秒。

(注意:本题要求对Time类使用运算符++)

#include<iostream>
using namespace std;

class Time
{
public:
    Time(){minute=0;sec=0;}
    Time(int m,int s):minute(m),sec(s){}
    Time operator ++();
    Time operator ++(int);
    void display(){cout<<minute<<":"<<sec<<endl;}
private:
    int minute;
    int sec;
};

Time Time::operator ++()
{
    if(++sec>=60)
    {
        sec-=60;
        ++minute;
    }
    return *this;
}

Time Time::operator ++(int)
{
    Time temp(* this);
    sec++;
    if(sec>=60)
    {
        sec-=60;
        ++minute;
    }
    return temp;
}

int main()
{
    Time time1(34,59),time2;
    cout<<"time1:";
    time1.display();
    ++time1;
    cout<<"++time1:";
    time1.display();
    time2=time1++;
    cout<<"time1++:";
    time1.display();
    cout<<"time2:";
    time2.display();
}


//输出结果
time1:34:59
++time1:35:0
time1++:35:1
time2:35:0

10.7重载流插入运算符“<<”和流提取运算符“>>”

如果想用“>>”和“<<”输出和输入自己声明的类型的数据,必须对它们重载

对“>>”和“<<”的重载函数形式如下:

istream & operator >>(istream &,自定义类 &);	//>>重载
ostream & operator <<(ostream &,自定义类 &);	//<<重载
  • 重载运算符“>>”的函数的第1个参数和函数的类型都必须是**istream&类型(即istream **类对象的引用),第2个参数是要进行输人操作的类。

  • 重载“<<”的函数的第1个参数和函数的类型都必须是** ostream&**类型,函数第2个参数是要进行输入操作的类。

  • 因此,只能将重载“>>”和“<<”的函数作为友元函数,而不能将它们定义为成员函数

10.7.1 重载流插入运算符“<<”

例,用重载运算符“<<”输出复数

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(){real=0;imag=0;}
    Complex(double r,double i){real=r;imag=i;}
    Complex operator +(Complex &c2);
    friend ostream& operator <<(ostream&,Complex&);
private:
    double real;
    double imag;
};

Complex Complex::operator +(Complex &c2)
{
    return Complex(real+c2.real,imag+c2.imag);
}

ostream & operator <<(ostream & output,Complex& c)
{
    output<<"("<<c.real<<"+"<<c.imag<<"i)"<<endl;
    return output;
}

int main()
{
    Complex c1(2,4),c2(6,10),c3;
    c3=c1+c2;
    cout<<c3;
    return 0;
}

//输出结果
(8+14i)
  • 程序中的**outputostream**类对象的引用,形参名output可以用户任意取

  • 调用函数时,形参成为实参**cout的引用,形参c成为c3**的引用。因此调用函数的过程相当于执行

    cout <<" ( " << c3. real << " + " <<cЗ, imag <<"i)" <<endl ;
    return cout;
    

    此时的“<<”是C++预定义的流插入符,因为它右侧的操作数是字符串常量和 double 类型数据

  • 其中,**return output**的作用为能连续向输出流插入信息

    output 是**ostream类的对象的引用(它是实参cout的引用,或者说output是cout的别名),cout 通过传送地址给 output,使它们二者共享同一段存储单元,因此,return output就是 return cout.将输出流cout **的现状返回,即保留输出流的现状。

10.7.2 重载流提取运算符“>>”

重载流提取运算符的目的是希望将>>”用于输人自定义类型的对象的信息。

例,增加重载流提取运算符“>>”,用“cin>>”输入复数,用”cout<<“输出复数

#include<iostream>
using namespace std;

class Complex
{
public:
    friend ostream& operator <<(ostream&,Complex&);
    friend istream& operator >>(istream&,Complex&);
private:
    double real;
    double imag;
};

ostream& operator <<(ostream& output , Complex &c)
{
    output<<"("<<c.real<<"+"<<c.imag<<"i)";
    return output;
}

istream& operator >>(istream& input,Complex& c)
{
    cout<<"input real part and imaginary part of complex number:";
    input>>c.real>>c.imag;
    return input;
}
int main()
{
    Complex c1,c2;
    cin>>c1>>c2;
    cout<<c1<<endl;
    cout<<c2<<endl;
    return 0;
}

//输入
3 6
4 10
//输出
input real part and imaginary part of complex number:3 6
input real part and imaginary part of complex number:4 10
(3+6i)
(4+10i)
  • 运算符“>>”重载函数中的形参input是istream 类的对象的引用,在执行cin >>c时,调用“ operator >>”函数,将 cin地址传递给 input,input是cin的引用,同样c是c1 的引用。

    因此,“input >>c.real >>c.imag;”相当于“ cin >>c1.real >>cl.inag;”

  • 以上运行结果无疑是正确的,但并不完善。在输入复数的虚部为正值时,输出的结果是没有问题的,但是虚部如果是负数,就不理想,请观察输出结果

    input real part and imaginary part of complex number :3 6 C1input real part and imaginary part of complex number:4 -10
    cl=(3 +6i)
    c2 =(4 +-10i)
    

    所以将输出函数修改为

    ostream& operator <<(ostream& output , Complex &c)
    {
        output<<"("<<c.real;
        if(c.imag>=0)
            output<<"+";
        output<<c.imag<<"i)"<<endl;
        return output;
    }
    

    最终函数:

    #include<iostream>
    using namespace std;
    
    class Complex
    {
    public:
        friend ostream& operator <<(ostream&,Complex&);
        friend istream& operator >>(istream&,Complex&);
    private:
        double real;
        double imag;
    };
    
    ostream& operator <<(ostream& output , Complex &c)
    {
        output<<"("<<c.real;
        if(c.imag>=0)
            output<<"+";
        output<<c.imag<<"i)"<<endl;
        return output;
    }
    
    istream& operator >>(istream& input,Complex& c)
    {
        cout<<"input real part and imaginary part of complex number:";
        input>>c.real>>c.imag;
        return input;
    }
    int main()
    {
        Complex c1,c2;
        cin>>c1>>c2;
        cout<<c1<<endl;
        cout<<c2<<endl;
        return 0;
    }
    
    //输入
    3 6 
    4 -10
    //输出
    input real part and imaginary part of complex number:3 6
    input real part and imaginary part of complex number:4 10
    (3+6i)
    
    (4+10i)
    

10.9 不同类型数据间的转换

C++提供类型转换函数(type conversion function)来解决这个问题。

类型转换函数的作用是将一个类的对象转换成另一类型的数据

如果已声明了一个Complex类,可以在Complex类中这样定义类型转换函数:

operator double( )
{return real;}

函数返回 double 型变量real的值。它的作用是将一个Complex类对象转换为一个double型数据,其值是Complex类中的数据成员 real的值。

类型转换函数的一般形式为:

operator 类型名()
{实现转换的语句}

注:

  • 在函数名前面不能指定函数类型,函数没有参数。

    其返回值的类型是由函数名中指定的类型名来确定的(例如前面定义的类型转换函数operator double,其返回值的类型是double)。

  • 类型转换函数只能作为成员函数,因为转换的主体是本类的对象。不能作为友元函数或普通函数。

转换构造函数和类型转换运算符有一个共同的功能:当需要的时候,编译系统会自动调用这些函数,建立一个无名的临时对象(或临时变量)。

例如,若已定义若已定义d1,d2为double 型变量,c1,c2为Complex类对象,如类中已定义了类型转换函数,若在程序中有以下表达式:

dl = d2 +c1;

编译系统发现“+”的左侧d2是 double型,而右侧的c1是Complex类对象

如果没有对运算符+进行重载,就会检查有无类型转换函数,结果发现了有对double的重载函数,就调用该函数,把 Complex 类对象 cl 转换为 double 型数据,建立了一个临时的 double 数据,并与 d2 相加,最后将一个 double 的值赋给 d1。

如果类中已定义了转换构造函数并且又重载了运算符“+”(作为Complex类的友元函数),但未对double定义类型转换函数(或者说未对double重载),若有以下表达式

c2=c1+d2;

它发现运算符“+“左侧的c1是complex类对象,右侧的 d2是double型。编译系统寻找有无对“+"的重载,发现有 operator+函数,但它是 Complex 类的友元函数,要求两个Complex类的形参,即只能实现两个Complex类对象相加,而现在d2是double 型,不符合要求。在类中又没有对 double 进行重载,因此不可能把c1 转换为couble 型数据然后相加。编译系统就去找有无转换构造函数,发现有,就调用转换构造医数Complex(d2),建立一个临时的Complex类对象,再调用operator+函数,将两个复数相加,然后赋给c2。相当于执行表达式:

c2=c1+Complex(d2);

例,将一个double数据与Complex类数据相加

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(){real=0;imag=0;}
    Complex(double r,double i){real=r;imag=i;}
    operator double (){return real;}
private:
    double real;
    double imag;
};

int main()
{
    Complex c1(3,4),c2(5,-10),c3;
    double d;
    d=2.5+c1;
    cout<<d<<endl;
    return 0;
}

//输出结果
5.5

程序分析:

  • 如果在 Complex 类中没有定义类型转换函数 operator double,程序编译将出错。

    因为不能实现double 型数据与Complex类对象的相加。现在,已定义了成员函数operator double,就可以利用它将 Complex类对象转换为 double 型数据。

    请注意,程序中不必显式地调用类型转换函数,它是自动被调用的,即隐式调用。

    编译系统在处理表达式2.5+c1时,发现运算符“+”的左侧是 double 型数据,而右侧是 Complex 类对象,又无运算符“+"重载函数,不能直接相加,编译系统发现有对 double的重载函数,因此调用这个函数,返回一个 double 型数据,然后与 2.5 相加。

  • 如果在main函数中加一个语句:

    c3=c2;

    由于赋值号两侧都是同一类的数据,是可以合法进行赋值的,没有必要把c2转换为 double列数据。

  • 如果在Complex,类中声明了重载运算符“+"函数作为友元函数:

    Complex operator +(Complex c1,Complex c2)
    {return Complex(c1.real+c2.real,c1.imag+c2.imag);}
    

    若在main函数中有语句

    c3=c1+C2;
    

    由于已对运算符+重载,使之能用于两个Complex类对象的相加,因此将cl和c2按
    Complex类对象处理,相加后赋值给同类对象c3

    如果改为

    d=c1+c2;	//d为double型变量
    

    将c1与c2两个类对象相加,得到一个临时的Complex类对象,由于它不能赋值给 double型变量,而又有对 double 的重载函数,于是调用此函数,把临时类对象转换为 double数据,然后赋给 d。

类型转换函数的好处:

  • 如果用类型转换函数对 double进行重载(使Complex类对象转换头double 型数据),就不必对各种运算符进行重载,因为Complex类对象可以被自动地转换为 double 型数据,而标准类型的数据的运算,是可以使用系统提供的各种运算符的。

例10.10,包含转换构造函数、运算符重载函数和类型转换函数的程序

#include<iostream>
using namespace std;

class Complex
{
public:
    Complex(){real=0;imag=0;}
    Complex(double r){real=r;imag=0;}
    Complex(double r,double i){real=r;imag=i;}
    
    friend Complex operator +(Complex c1,Complex c2);
    void display();
private:
    double real;
    double imag;
};

Complex operator +(Complex c1,Complex c2)
{
    return Complex(c1.real+c2.real,c1.imag+c2.imag);
}

void Complex::display()
{
    cout<<"("<<real<<","<<imag<<"i)"<<endl;
}

int main()
{
    Complex c1(3,4),c2(5,-10),c3;
    c3=c1+2.5;
    c3.display();
    return 0;
}

//输出结果
(5.5,4i)

程序分析:

  1. 如果没有定义转换构造函数,则此程序编译出错,因为没有重载运算符使之能将Complex 类对象与 double 数据相加。

    由于c3 是Complex 类对象,先将 2.5,转换为 Complex 类对象,然后与 c1相加,再赋值给 c3。

  2. 由于已重载了运算符“+”,在处理表达式 c1+2.5时,编译系统把它解释为

    operator +(c1,2.5);
    

    由于2.5不是 Complex类对象,系统先调用转换构造函数Complex(2.5),建立一个临时的 Complex 类对象,其值为(2.5+0i)。上面的函数调用相当于

    operator +(c1,Complex(2.5));
    

    将c1与(2.5+0i)相加,赋给c3。

  3. 如果把“c3=c1+2.5”改为

    c3=2.5+c1;
    

    结果与前面相同,说明此函数适用交换律

    从中得到一个重要结论:

    在已定义了相应的转换构造函数情况下,将运算符“+”函数重载为友元函数,在进行两个复数相加时,可以用交换律。

结论:

  • 如果运算符函数重载为成员函数,它的第1个参数必须是本类的对象。
  • 当第1个操作数不是类对象时,不能将运算符函数重载为成员函数,而是友元函数
  • 如果将运算符“+”函数重载为类的成员函数,交换律不适用。
  • 一般将双目运算符函数重载为友元函数,单目运算符重载为成员函数
  1. 如果在上面程序的基础上增加类型转换函数:

    operator double(){return real;}
    

    此时Complex类的公用部分为

    public:
    Complex(){real=0;imag=0;}					//默认构造函数,无形参
    Complex(double r){real=r;imag=0;}			//转换构造函数,一个形参
    Complex(doubel r,double i){real=r;imag=i;}	//实现初始化的构造函数,两个形参
    operator double(){return real;}				//烈性转换函数,无形参
    friend Complex operator +(Complex c1,Complex c2);	//重载运算符“+”的友元函数
    void display();
    

    此时,程序在编译时出错,原因是在处理c1+2.5时出现二义性。

    一种理解是,调用转换构造函数,把2.5变成Complex 类对象,然后调用运算符“+”重载函数,与。进行复数相加。

    另一种理解是,调用类型转换函数,把c1转换为 double 型数,然后与2,5进行相加。

    系统无法判定,这二者是矛盾的。

5. 继承与派生

11.1 派生类的声明方式

声明派生类的一般形式为:

class 派生类名:[继承方式] 基类名
{
    派生类新增加的成员;
};

例:声明一个基类 Student,在此基础上通过单继承建立一个派生类 Student1

class Student1:public Student
{
public:
    void display_1()
    {
        cout<<"age:"<<age<<endl;
        cout<<"address:"<<addr<<endl;
    }
private:
    int age;
    string addr;
};

其中,public表示公有继承

继承方式包括:public(公有的)private(私有的)protected(受保护的)

继承方式是可选的,如果不写此项,则默认为private(私有的)

11.2 派生类的构成

派生类中的成员主要包括:

  • 从基类继承过来的成员
  • 自己增加的成员

从基类继承的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性。正是这些新增加的成员体现了派生类与基类的不同,体现了不同派生类之间的区别。

11.4 派生类成员的访问属性

  • 公有继承

    基类的公有成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有

  • 私有继承

    基类的公有成员和保护成员在派生类中成了私有成员。其私有成员仍为基类私有

  • 受保护的继承

    基类的公有成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。

    保护成员的意思是,不能被外界引用,但可以被派生类的成员引用。

派生类中访问属性在派生类中在派生类外部在下一层公用派生类中
公用可以可以可以
保护可以不可以可以
私有可以不可以不可以
不可访问不可以不可以不可以

11.5 派生类的构造函数和析构函数

11.5.1 简单的派生类的构造函数

派生类构造函数一般形式为:

派生类构造函数名(总参数表):基类构造函数名(参数表)
{
    派生类中新增成员初始化语句;
}

例子:

Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s){
    age=a;
    addr=ad;
}

11.5.2 有子对象的派生类的构造函数

定义派生类构造函数的一般形式为:

派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
{
    派生类中新增数据成员初始化语句;
}

执行派生类构造函数的顺序:

  1. 调用基类构造函数,对基类数据成员初始化
  2. 调用子对象构造函数,对子对象数据成员初始化
  3. 再执行再执行派生类构造函数本身,对派生类数据成员初始化

例:派生类Student1中定义了一个子对象monitor

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

class Student
{
public:
    Student(int n,string nam)
    {
        num=n;
        name=nam;
    }
    void display()
    {
        cout<<"num:"<<num<<endl<<"name:"<<name<<endl;
    }
protected:
    int num;
    string name;
};

class Student1:public Student
{
public:
    Student1(int n,string nam,int n1,string name1,int a,string ad)
    :Student(n,nam),monitor(n1,nam1)
    {
        age=a;
        addr=ad;    
    }
private:
    Student monitor;
    int age;
    string addr;
};

11.5.3 多层派生时的构造函数

基类的构造函数首部:

Student(int n,string nam)

派生类Student1的结构函数首部:

Student1(int n,string nam,int a):Student(n,nam)

派生类Student2的构造函数首部:

Student2(int n,string nam,int a,int s):Student1(n,nam,a)

注:不需要列出每一层派生类的构造函数,只需写出其上一层派生类(即它的直接基类)的构造函数即可

初始化的顺序:

  1. 先初始化基类的数据成员num和name
  2. 再初始化Student1的数据成员age
  3. 最后再初始化Student2的数据成员score

11.5.4 派生类构造函数的特殊形式

  • 当不需要对派生类新增的成员进行任何初始操作时,派生类构造函数的函数体可以为空,即构造函数是空函数,如

    Student1(int n,string nam,int n1,string nam1):Student(n,nam),monitor(n1,nam1){}
    
  • 如果在基类中没有定义构造丽数,或定义了没有参数的构造函数,那么,在定义派生类构造函数时可以不写基类构造函数。

11.6 多重继承

11.6.1 声明多重继承的方法

如果已声明了类A、类B和类C,可以声明多重继承的派生类D:

class D:public A,private B,protected C
{
    类D新增加的成员
}

D是多重继承的派生类,它以公用继承方式继承类A,以私有继承方式继承类B,以保护继承方式继承类C。

11.6.2 多重继承派生类的构造函数

​ 多重继承派生类的构造函数形式与单继承时的构造函数形式基本相同,只是在初始表中包含多个基类构造函数。如

派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数表列)
{
    派生类中新增数据成员初始化语句;
}

例11.8:

声明一个教师(Teacher)类和一个学生(Student)类,用多重继承的方式声明一个在职研究生派生类(在职教师攻读研究生)。教师类中包括数据成员name、age、title(职称)学生类中包括数据成员name1(姓名)、age(性别)、score(成绩)

class Graduate:public Teacher,public Student
{
    public:
    Graduate(string nam,int a,char s,string t,float sco,float w):
    	Teacher(nam,a,t),Student(nam,s,sco),wage(w){}
    void show();
    private:
    float wage;
};

11.6.3 多重继承引起的二义性问题

  1. 两个基类有同名成员

    class A
    {
    public:
        int a;
        void display();
    };
    
    class B
    {
    public:
        int a;
        void display();
    };
    
    class C:public A,public B   
    {
    public:
        int b;
        void show();
    };
    
    int main()
    {
        C c1;
        c1.a=3;
        c1.display();
    }
    

    上述程序,由于基类A和基类B都有数据成员a和成员函数display,编译系统无法判别要访问的是哪一个基类的成员

    所以需要加基类名来限定,修改为:

    c1.A::a=3;
    c1.A::display();
    

    如果派生类C中的成员函数show访问基类A的display和a,可以不必写对象名而直接写类名,如

    A::a=3;			//指当前对象
    A::display();
    
  2. 两个基类和派生类三者都有同名成员

    将上述的C类声明改为:

    class C:public A,public B
    {
        int a;
        void display();
    };
    

    此时有3个display函数

    C类
    int a;
    int A::a;
    int B::a;
    void display();
    void A::display();
    void B::display();

    main函数改为:

    C c1;
    c1.a=3;
    c1.display();
    

    此时访问的是派生类中的display函数

    因为在C++中有这样的规则:

    派生类新增加的同名成员覆盖了基类中的同名成员。

    (注意:不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖;如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。)

  3. 如果类A 和类B是从同一个基类派生的

    如图

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

class N
{
public:
    int a;
    void display(){cout<<a<<endl;}
};

class A:public N
{
public:
    int a1;
};

class B:public N
{
public:
    int a2;
};

class C:public A,public B
{
public:
    int a3;
    void show(){cout<<a3<<endl;}
};

int main()
{
    C c1;
}

在类A和类B中虽然没有定义数据成员a和成员函数 display;

但是它们分别从类N继承了数据成员a和成员函数display;

这样在类A和类B中同时有在看两个同名的类成员a和成员函数 display。

那怎样访问类A中从基类N继承下来的成员呢?

通过类N的直接派生类名来支出要访问的是类N的哪一个派生类中的基类成员,如

c1.A::a=3;
c1.A::display();

11.6.4 虚基类

  1. 虚基类的作用

    C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员

  2. 虚基类的声明方法:

    class A
    {...};
    class B:virtual public A
    {...};
    class C:virtual public A
    {...};
    

    注意:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的

  3. 声明虚基类的一般形式为:

    class 派生类名:virtual 继承方式 基类名
    

    经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次

    需要注意,为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类横中声明为虚基类。否则仍然会出现对基类的多次继承

  4. 虚基类的初始化:

    class A
    {
        A(int i){}
    };
    
    class B:virtual public A
    {
        B(int n):A(n){}
    }
    
    class C:virtual public A
    {
        C(int n):A(n){}
    }
    
    class D:public B,public C
    {
        D(int n):A(n),B(n),C(n)
    }
    

    **注意:**在定义D类构造函数时,与以往使用的方法有所不同。以前,在派生类的构造商数中只须负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。

    现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。

    所以规定:

    在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化

  5. 虚基类的简单应用举例

    例11.9 在例11.8的基础上,在Teacher 类和 Student类之上增加一个共同的基类Person,如图11.25所示。作为人员的一些基本数据都放在Person中,在Teacher 类和 Student 类中再增加一些必要的数据。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    #include<iostream>
    #include<string>
    using namespace std;
    
    class Person
    {
    public:
        Person(string nam,char s,int a)
        {
            name=nam;
            sex=s;
            age=a;
        }
    protected:
        string name;
        char sex;
        int age;
    };
    
    class Teacher:virtual public Person
    {
    public:
        Teacher(string nam,char s,int a,string t):Person(nam,s,a){title=t;}
    protected:
        string title;
    };
    
    class Student:virtual public Person
    {
    public:
        Student(string nam,char s,int a,float sco)
        :Person(nam,s,a),score(sco){}
    protected:
        float score;
    };
    
    class Graduate:public Teacher,public Student
    {
    public:
        Graduate(string nam,char s,int a,string t,float sco,float w)
        :Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){}
        void show()
        {
            cout << " name :" << name << endl;
            cout << " age :" << age << endl;
            cout << " sex :" << sex << endl ;
            cout << " score : " << score << endl;
            cout << " title :" << title <<endl;
            cout <<" wages :" << wage << endl;
        }
    private:
        float wage;
    };
    
    int main()
    {
        Graduate grad1("Wang_li", 'f' , 24,"assistant",89.5,1200);
        grad1.show();
        return 0;
    }
    
    // 输出结果
     name :Wang_li
     age :24
     sex :f
     score : 89.5
     title :assistant
     wages :1200
    

6、 多态和虚函数

C++中,多态性表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数

6.1 利用虚函数实现动态多态性

通过指针调用不同层次的同名函数,如

pt->display();可以调用不同派生层次中的 display 函数,只需在调用前临时给指针变量pt赋予不同的值(使之指向不同的类对象)

具体程序实现:

Student是基类,Graduate是派生类,它们都有display这个同名的函数,实现对两个类的输出

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

class Student
{
protected:
    int num;
    string name;
    float score;
public:
    Student(int ,string ,float);
    virtual void display();
};

Student::Student(int n,string nam,float s)
{
    num=n;
    name=nam;
    score=s;
}

void Student::display()
{
    cout<<"num:"<<num<<"\nname:"<<name<<" \nscore:"<<score<<"\n\n";
}

class Graduate:public Student
{
private:
    float wage;
public:
    Graduate(int n,string nam,float s,float w):Student(n,nam,s),wage(w){}
    void display();
};

void Graduate::display()
{
    cout<<"num:"<<num<<"\nname:"<<name<<" \nscore:"<<score<<"\nwage = "<<wage<<endl;
}

int main()
{
    Student stud1(1001 , "Li" , 87.5);
    Graduate grad1(2001 ,"Wang",98.5,1200);
    Student* pt = &stud1;
    pt->display();
    pt=&grad1;
    pt->display();
    return 0;
}

//输出结果
num:1001
name:Li
score:87.5

num:2001
name:Wang
score:98.5
wage = 1200

用同一种调用形式pt->display(),而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数,这就是多态性

**说明:**本来,Student类的 display 函数前没有加 virtual ,基类指针是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类的对象的指针先转换为基类的指针,这样基类指针指向的是派生类对象中的基类部分

在基类中的 display 被声明为虚函数,在声明派生类时被重载,这是派生类的同名函数 display 就取代了其基类中的虚函数。因此在使基类指针指向派生类对象后,调用 display 函数时就调用了派生类的 display 函数

要注意,如果不声明为虚函数,试图通过基类指针调用派生类的非虚函数是不行的

虚函数的使用方法:

  1. 在基类中用 virtual 声明成员函数为虚函数,在类外定义虚函数时,不必再加virtual

  2. 在派生类中重新定义此函数时,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体

    习惯上在每一层声明该函数时都加上 virtual

    如果派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数

  3. 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象

  4. 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数

注意:

  • 在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数;
  • 只能用 virtual 声明类的成员函数,把它作为虚函数

6.2 虚析构函数

代码示例:

#include<iostream>
using namespace std;

class Point
{
public:
    Point(){}
    virtual ~Point(){cout<<"executing Point destructor"<<endl;}
};

class Circle:public Point
{
public:
    Circle(){}
    ~Circle(){cout<<"executing Circle destructor"<<endl;}
private:
    int radius;
};

int main()
{
    Point* p=new Circle;
    delete p;
    return 0;
}

//输出结果
executing Circle destructor
executing Point destructor
    
//如果没加virtual
executing Point destructor

6.3 纯虚函数与抽象类

纯虚函数

一般形式:

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

具体:

virtual float area() const =0;

注意:

  • 纯虚函数没有函数体
  • 这是一个声明语句,最后应有分号
  • 最后“=0”并不表示函数返回值为0,它只起形式上的作用

抽象类

有时,定义一些类的唯一目的是用它作为基类去建立派生类

实用实例

#include<iostream>
using namespace std;

class Shape
{
public:
    virtual float area() const =0;
    virtual float volume() const =0;
    virtual void shapeName() const =0;
};

class Point:public Shape
{
protected:
    float x,y;
public:
    Point(float a,float b):x(a),y(b){}
    void setPoint(float a,float b){x=a;y=b;}
    float getX() const {return x;}
    float getY() const {return y;}
    virtual float area() const{return 0;}
    virtual float volume() const{return 0;}
    virtual void shapeName() const {cout<<"Point:";}


    friend ostream& operator <<(ostream& ,const Point&);
};

ostream& operator <<(ostream& output,const Point& p)
{
    output<<"["<<p.x<<","<<p.y<<"]";
    return output;
}

class Circle:public Point
{
protected:
    float radius;
public:
    Circle(float x=0,float y=0,float r=0):Point(x,y),radius(r){}
    void setRadius(float r){radius=r;}
    float getRadius() const{return radius;}
    virtual void shapeName() const {cout<<"Circle:";}
    virtual float area() const{return 3.14159*radius*radius;}
    friend ostream& operator <<(ostream& output,const Circle& c);
};

ostream& operator <<(ostream& output,const Circle& c)
{
    output<<"["<<c.x<<","<<c.y<<"],r="<<c.radius;
    return output;
}

class Cylinder:public Circle
{
protected:
    float height;
public:
    Cylinder(float x=0,float y=0,float r=0,float h=0):Circle(x,y,r),height(h){}
    void setHeight(float h){height=h;}
    virtual float area() const {return 2*Circle::area()+2*3.14159*radius*height;}
    virtual void shapeName() const {cout<<"Cylinder:";}
    virtual float volume() const {return Circle::area()*height;}
    friend ostream& operator <<(ostream& output,const Cylinder& c);
};

ostream& operator <<(ostream& output,const Cylinder& c)
{
    output<<"["<<c.x<<","<<c.y<<"],r="<<c.radius<<",h="<<c.height;
    return output;
}

int main()
{
    Point  point(3.2,4.5);
    Circle circle(2.4,1.2,5.6);
    Cylinder ctlinder(3.5,6.4,5.2,10.5);
    point.shapeName();
    cout<<point<<endl;

    circle.shapeName();
    cout<<circle<<endl;

    ctlinder.shapeName();
    cout<<ctlinder<<endl;

    Shape* pt;
    pt = &point;
    pt->shapeName();

    pt=&circle;
    pt->shapeName();

    pt=&ctlinder;
    pt->shapeName();

    return 0;
}

//输出结果
Point:[3.2,4.5]
Circle:[2.4,1.2],r=5.6
Cylinder:[3.5,6.4],r=5.2,h=10.5
Point:Circle:Cylinder:

7、 String函数(补充)

  1. string 和 char 的区别*

    • char*是一个指针

    • String是一个类,其内部封装好了char *,是一个char *型的容器

  2. 特点

    • String类内部封装了很多方法

      例如:查找find,拷贝copy、删除delete、替换replace、插入insert

    • string管理char*所分配的内存,不用担心复制越界和取值越界等问题

  3. string容器的构造方法

    • 默认构造

      string s1;
      
    • 用字符串s初始化

      const char* s="hello world";
      string s2(s);
      cout<<"s2="<<s2<<endl;
      
    • 用n个指定字符来初始化

      string s4(10,'a');
      cout<<"s4="<<s4<<endl;
      

1. string容器的赋值方式

  • = 方法

    string str1;
    str1 = "hello world!";
    cout<<"str1 = "<<str1<<endl;
    
    string str2;
    str2=str1;
    cout<<"str2 = "<<str2<<endl;
    
    string str3;
    str3 = 'a';
    cout<<"str3 = "<<str3<<endl;
    
    //输出
    str1 = hello world!
    str2 = hello world!
    str3 = a
    
  • assign()函数方式

    string str4;
    str4.assign("hello c++");
    cout<<"str4 = "<<str4<<endl;
    
    //取字符串str的前7个字符
    string str5;
    str5.assign("hello c++",7);
    cout<<"str5 = "<<str5<<endl;
    
    //将str5复制到str6
    string str6;
    str6.assign(str5);
    cout<<"str6 = "<<str6<<endl;
    
    //将5个x赋给str7
    string str7;
    str7.assign(5,'x');
    cout<<"str7 = "<<str7<<endl;
    
    //输出
    str4 = hello c++
    str5 = hello c
    str6 = hello c
    str7 = xxxxx
    

2.string容器的字符串拼接

  • += 方法

    string str1 = "hello";
    str1 += " world";	//在str1的后面拼接
    cout<<"str1 = "<<str1<<endl;
    
    str1 += '!';	//也可以直接加一个字符
    cout<<"str1 = "<<str1<<endl;
    
    string str2 = "233";
    str1 += str2;
    cout<<"str1 = "<<str1<<endl;
    
    //输出
    str1 = hello world
    str1 = hello world!
    str1 = hello world!233
    
  • append ()函数方法

    string str3 = "hello";
    str3.append(" world");
    cout<<"str3 = "<<str3<<endl;
    //str3 = hello world
    
    str3.append("123456",3);	//只拼接前3个字符
    cout<<"str3 = "<<str3<<endl;
    //str3 = hello world123
    
    string str4 = "str4";
    str4.append(str3);
    cout<<"str4 = "<<str4<<endl;
    //str4 = str4hello world123
    
    //截取str3的字符,范围是从下标为0的字符到下标为5的字符
    str4.append(str3,0,5);
    cout<<"str4 = "<<str4<<endl;
    //str4 = str4hello world123hello
    
    //也可以直接将字符本身放进函数中
    str4.append("hello world",0,5);
    cout<<"str4 = "<<str4<<endl;
    //str4 = str4hello world123hellohello
    
    //输出结果
    str3 = hello world
    str3 = hello world123
    str4 = str4hello world123
    str4 = str4hello world123hello
    str4 = str4hello world123hellohello
    

3. string容器的字符串查找

  • find()函数

    • 只有一个参数,就是待查找的目标字符串

    • 返回值:

      若找到,则返回目标字符串第一次出现的 下标位置(从0开始)

      反之,返回-1

  • rfind()函数

    • 参数与find()函数相同

    • find()函数是在原字符串中从左到右依次查找

      rfind()函数实在原字符串在从右到左依次查找

    • 返回值:

      若找到,则返回目标字符串第一次出现的 下标位置

      反之,返回-1

  • 代码示例

    void test04()
    {
        //string 容器的字符串的查找
        string str1 = "abcdefgdede";
        int pos1 = str1.find("de");
        if(pos1 == -1)
        {
            //若未找到目标字符串,则返回 -1 
            cout<<"未找到字符串"<<endl;
        }
        else
        {
            //pos返回的是从左往右找的过程中第一次遇到 目标字符串的 第一个字符的下标位置(不是位序)
            cout<<"找到字符串,pos1 = "<<pos1<<endl;
        }
        int pos2 = str1.rfind("de");
        if(pos2 == -1)
        {
            cout<<"未找到字符串"<<endl;
        }
        else
        {
            //rfind()函数于find()函数几乎相同,只是rfind()函数是从右往左搜索,而find()函数是从左往右搜索
            cout<<"找到字符串,pos2 = "<<pos2<<endl;
        }
    }
    
    //输出结果
    找到字符串,pos1 = 3
    找到字符串,pos2 = 9
    

4. string 容器的字符串替换

  • replace()函数

    共有三个参数:

    • 第一个参数:开始位置的下标
    • 第二个参数:替换掉原字符串的长度
    • 第三个参数:用来替换的字符串
  • 代码示例

    void test05()
    {
        //string 容器的字符串的替换
        string str1 = "abcdefgdedee";
        //replace()函数
        //第一个参数是从该 下标 开始
        //第二个参数是替换原字符串的多少个字符
        //第三个参数是 拿什么字符串替换
        str1.replace(1,3,"111111111");
        cout<<"str1 = "<<str1<<endl;
    }
    
    //输出结果
    str1 = a111111111efgdedee
    

5.string 容器的字符串比较

  • compare()函数

    • 只有一个参数,那就是待比较的字符串
    • 结果有三种情况
      1. = 相等,返回0
      2. ”>“ 大于,返回1
      3. “<” 小于,返回-1
  • 代码示例:

    void test06()
    {
        //string 容器的字符串比较
        //比较方式 按字符的 ASCII码值比较
        // = 返回 0
        // > 返回 1
        // < 返回 -1
        string str1 = "Hello";
        string str2 = "hello";
        
        int ret = str1.compare(str2);
        if(ret == 0)
        {
            //通常使用字符串比较只用比较其是否相等即可,至于谁大谁小意义不大
            cout<<"str1 == str2"<<endl;
        }
        else
        {
            cout<<"str1 != str2"<<endl;
        }
    }
    
    //输出结果
    str1 != str2
    

6.string 容器的字符串存取

  1. 用 [ ] 方法

    直接获取指定位置的字符

  2. 用 at() 函数

    只有一个参数,那就是待获取的位置

  3. 代码示例

    void test07()
    {
        //string 容器的字符(串)存取
        string str1 = "algorithm";
        
        //用 [] 方法
        for(int i=0;i<str1.size();i++)
        {
            cout<<str1[i]<< " ";
        }
        cout<<endl;
    
        //用 at() 函数方法
        for(int i=0;i<str1.size();i++)
        {
            cout<<str1.at(i)<<" ";
        }
        cout<<endl;
    
        //xlgorithm
        str1.at(0) = 'x';
        cout<<str1<<endl;
    
        //xxgorithm
        str1[1] = 'x';
        cout<<str1<<endl;
    }
    
    //输出结果
    a l g o r i t h m
    a l g o r i t h m
    xlgorithm
    xxgorithm
    

7. string 容器的字符串的插入和删除

  1. 插入

    有两个参数:

    ​ 第一个参数:原字符串待插入的下标位置

    ​ 第二个参数:插入的字符串

  2. 删除

    有两个参数:

    ​ 第一个参数:原字符串待删除的下标位置

    ​ 第二个参数:删除的字符个数

  3. 代码示例:

    void test08()
    {
        //string 容器的元素的插入和删除
        string str1 = "algorithm";
        str1.insert(1,"1111");
        //在下标 1 后插入指定字符串
        cout<<"插入后,str1 = "<<str1<<endl;
    
        str1.erase(1,4);
        //在下标 1 后删除 4 个字符
        cout<<"删除后,str1 = "<<str1<<endl;
    
        //*******************************
        //插入和删除的起始下标都是从 0 开始
        //*******************************
    }
    
    //输出结果
    插入后,str1 = a1111lgorithm
    删除后,str1 = algorithm
    

8. string 容器的字符串取子串

  1. substr( )函数

    有两个参数

    ​ 第一个参数:原字符串中去子串的起始位置的下标

    ​ 第二个参数:截取子串的长度

  2. 代码示例:

    void test09()
    {
        //string 容器截取字串substring
        string str1 = "algorithm";
        //第一个参数是起始位置的下标
        //第二个参数是截取 子串的长度
        string substr1 = str1.substr(0,6);
        cout<<"substr1 = "<<substr1<<endl;
        //截取后对原字符串无影响
        cout<<"str1 = "<<str1<<endl;
    
        //实际应用示例
        string email = "string@sina.com";
        int pos = email.find("@");
        string userName = email.substr(0,pos);
        cout<<"userName:"<<userName<<endl;
    }
    
    //输出结果
    substr1 = algori
    str1 = algorithm
    userName:string
    

8、多种输入(补充)

cin.ignore()

函数原型:

istream& ignore(int count =1 , int delim = EOF);

参数解析:

  • count参数:表示要忽略的字符数量,默认值为1
  • delim:表示停止忽略的条件,默认值为EOF,表示文件结束;
  • 即cin.ignore() 会一直忽略字符,知道满足两个参数条件中的一个

**返回值解析:**该函数返回一个引用到调用它的流对象,所以可以连接到其他输入流操作中

两种形式:

  • cin.ignore(num);
  • cin.ignore(num,delim);

代码示例:

#include<iostream>
using namespace std;

int main()
{
    char buf[32];
    char buf2[32];

    cin>>buf;	//"hello"
    
//情况一
    cin.ignore(3);	//忽略三个字符
//  cin.getline(buf2,32);	//输入" world",实际只有rld
    cin>>buf2;  //同理
    
    
//情况二
    cin.igonre(3,'w');
    cin>>buf2;	//orld

    cout<<"buf="<<buf<<endl;
    cout<<"buf2="<<buf2<<endl;

    system("pause");

    return 0;
}

cin.get()

此函数从输入流中读入一个字符(char非int,如果定义数组或变量为int就会出错),返回值为该字符的ASCII码如果碰到输入的末尾,返回值为EOF

1) cin.get(字符变量名)

用来接受字符,只获取一个字符可以接受空格遇到回车结束

代码示例:

#include<iostream>
using namespace std;

int main()
{
    char c[20];
    for(int i=0;i<20;i++)
        cin.get(c[i]);
    for(int i=0;i<20;i++)
        cout<<c[i];

    return 0;
}

2) cin.get(数组名,接收字符数目)

用来接受字符串可以接受空格遇到回车结束

注意: 数组的最后一位为‘\0’,如果设置接受n个字符,最多只能接受n-1而输入了大于等于n个字符,实际接受的只有n-1个字符,第n个为‘\0’(包括空格,不包括回车)所以预设数组大小时要比实际输入大小大1

代码示例:

#include<iostream>
using namespace std;

int main()
{
    char c[3];
    cin.get(c,3);
    cout<<c;
    return 0;
}

3) cin.get()

无参数主要用于舍弃输入流中不需要的字符,或舍弃回车

cin.getline()

用于读取字符数组

有两个重载版本:

istream& getline(char* buf,int bufSize);	//读取bufSize-1个字符到缓冲区buff,或遇到\n为止(哪个条件先满足就停止输入),函数会自动在buf中读入数据的结尾添加\0
istream& getline(char* buf,int bufsize,char delim);		//读取bufSize-1个字符到缓冲区buff,或遇到delim为止(\n和delim不会被读入buff,但会被从输入流中取走,哪个条件先满足就停止输入),函数会自动在buf中读入数据的结尾添加\0

1)cin.getline(变量名,输入大小)

代码示例:

#include<iostream>
using namespace std;

int main()
{
    char c[3];
    cin.getline(c,3);
    cout<<c<<endl;
    return 0;
}

//输入
123456
//输出
12

2)cin.getline(变量名,输入大小,结束标志)

代码示例:

#include<iostream>
using namespace std;

int main()
{
    char c[5];
    cin.getline(c,5,'3');
    cout<<c<<endl;
    return 0;
}

//输入
123456
//输出
12

getline()

用于string类接受一个字符串可以接收空格、回车

不会在数组结尾加’\0‘

使用时需要包含头文件#include<string>

格式:

//格式一
getline(cin,string s);
//格式二
getline(cin,string s,char ch);	//ch为结束标志位,结束符不放在缓存区

代码示例:

#include <iostream>
#include <string> 
using namespace std;
 
int main()
{
    string name;
    getline(cin, name);
    cout << name << endl;
    return 0;
}

//输入
harry poter
//输出
harry poter
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string name;
    getline(cin, name,'e');
    cout << name << endl;
    return 0;
}

//输入
harry poter
//输出
harry pot

9、容器

1)基本概念

功能:vector数据结构和数组非常相似,也称为单端数组

与普通数组区别:数组是静态空间(大小固定),而vector可以动态扩展

(1)不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝到新空间,释放原空间

(2)vector容器的迭代器是支持随机访问的迭代器

2)vector 构造函数

**功能:**创建vector容器

函数原型:

  •   vector<T> v;	//采用模板实现类实现,默认构造函数
    
  •   vector(v.begin(),v.end());	//将v(begin(), end()) 区间中的元素拷贝给本身 (前闭后开)
    
  •   vector(n,elem);	//构造函数,将n个elem拷贝给自身,elem数组类型根据vector的类型决定
    
  •   vector(const vector &vec);	//拷贝构造函数
    

代码示例:

//vector单端数组构造函数

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

void Fun_Print(vector<int>& v)
{
    vector<int>::iterator it;    // vector<int>::iterator 拿到vector<int>这种容器的迭代器类型;
                                     //每个容器都有一个专属的迭代器类型
    for(it=v.begin() ; it!=v.end() ; it++)
        cout<<*it<<" ";     // it是个迭代器类型,本是是个指针,需使用*解引用
    cout<<endl;
}

void test()
{
    // 1、默认构造,无参构造
    vector<int> v;

    // 2、将v开始迭代器到结束迭代器之间的元素,赋给v1
    vector<int> v1(v.begin() , v.end());

    // 3、将10个10拷贝给v
    vector<int> v2(10,10);
    Fun_Print(v2);  // 调用Fun_Print()函数,遍历v2

    // 4、拷贝构造
    vector<int> v3(v2);
    Fun_Print(v3);
}

int main()
{
    test();
    cout<<endl;
    return 0;
}

3)vector赋值操作

**功能:**给vector容器进行赋值

函数原型:

  •   vector operator =(const vector &vec);	//重载等号运算符
    
  •   assign(beg,end);	//将[beg,end)区间的数据拷贝赋值给本身
    
  •   assign(n,elem);		//将n个elem拷贝给本身
    

代码示例:

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

void Fun_Print(vector<int>& v)
{
    vector<int>::iterator it;
    for(it=v.begin() ; it!=v.end() ; it++)
        cout<<*it<<" ";
    cout<<endl;
}

void test()
{
    // 默认构造,无参构造
    vector<int> v;
    for(int i=0;i<10;i++)
        v.push_back(i);

    // 1、赋值 operator=
    vector<int> v1=v;
    Fun_Print(v1);

    // 2、使用assign()函数,将容器v起始迭代器到结束迭代器之间的元素赋给v1
    vector<int> v2;
    v2.assign(v.begin(),v.end());
    Fun_Print(v2);

    // 3、使用assign()函数,将10个'a'赋值给本身
    vector<int> v3;
    v3.assign(10,'a');
    Fun_Print(v3);
}

int main()
{
    test();
    cout<<endl;
    return 0;
}

//输出
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
97 97 97 97 97 97 97 97 97 97

4)vector容量和大小

**功能:**对vector容器的容量和大小操作

函数原型:

  •   empty();	//判断容器是否为空
    
  •   capacity();	//容器的容量
    
  •   size();	//返回容器中元素的个数
    
  •   resize(int num); 	//重设容器的大小为num
      //若容器变长,则以默认值填充新位置 (默认填充0)
      //如果容器变短,则末尾超出容器长度的元素被删除
    
  •   resize(int num,T elem);
      //重新设定容器的长度为num,若容器变长,则以elem值填充新位置
      //如果容器变短,则末尾超出容器长度的元素被删除
    

代码示例:

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

void Fun_Print(vector<int>& v)
{
    vector<int>::iterator it;
    for(it=v.begin() ; it!=v.end() ; it++)
        cout<<*it<<" ";
    cout<<endl;
}

void test()
{
    // 创建vector单端数组容器对象,并且通过模板参数指定容器中存放的数据的类型
    vector<int> v;

    v.push_back(1);
    v.push_back(2);
    v.push_back(3); //v=1 2 3
    v.push_back(4);

    if(v.empty())
        cout<<"v容器为空"<<endl;
    else
    {
        cout<<"容器不为空"<<endl;

        // 2、capacity()返回容器的容量
        cout<<"v容器的容量为:"<<v.capacity()<<endl;

        // 3、size()返回容器的个数
        cout<<"v容器的个数为:"<<v.size()<<endl;
    }

    cout<<endl;

    // 4.1、resize()重新指定大小 ,若指定的更大,默认用0填充新位置
    v.resize(10);
    Fun_Print(v);

    // 4.2、resize()重新指定大小 ,若指定的更小,超出部分元素被删除
    v.resize(5);
    Fun_Print(v);


    // 5.1、resize()重新指定大小 ,若指定的更大,默认用0填充新位置;可以利用重载版本替换默认填充,默认填充10
    v.resize(10,10);
    Fun_Print(v);

    // 5.2、resize()重新指定大小 ,若指定的更小,超出部分元素被删除
    v.resize(5,50);
    Fun_Print(v);
}

int main()
{
    test();
    cout<<endl;
    return 0;
}

//输出结果
容器不为空
v容器的容量为:4
v容器的个数为:4

1 2 3 4 0 0 0 0 0 0
1 2 3 4 0
1 2 3 4 0 10 10 10 10 10
1 2 3 4 0

5)vector插入和删除

**功能:**对vector容器进行插入、删除操作

函数原型:

  •   push_back(ele);	//尾部插入元素ele
    
  •   pop_back();	//删除最后一个元素
    
  •   insert(const_iterator pos,T ele);	//迭代器指向位置pos插入元素ele
    
  •   insert(const_iterator pos,int count,T ele);	//迭代器指向位置pos插入count个元素ele
    
  •   erase(conset_iterator pos);	//删除迭代器指向的元素
    
  •   erase(const_iterator start,const_iterator end);	//删除迭代器从start到end之间的元素
    
  •   clear();	//删除容器中所有元素
    

代码示例:

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

void Fun_Print(vector<int>& v)
{
    vector<int>::iterator it;
    for(it=v.begin() ; it!=v.end() ; it++)
        cout<<*it<<" ";
    cout<<endl;
}

void test()
{
    // 创建vector单端数组容器对象,并且通过模板参数指定容器中存放的数据的类型
    vector<int> v;

    // 1、push_back()向容器尾部插入元素
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    Fun_Print(v);   //1 2 3

    // 2、pop_back()插入容器尾部元素
    v.pop_back();
    Fun_Print(v);   //1 2

    // 3、insert()在容器的结束迭代器处,插入100
    v.insert(v.end(),100);
    Fun_Print(v);   //1 2 100

    // 4、insert()在容器的结束迭代器处,插入5个1000
    v.insert(v.end(),5,1000);
    Fun_Print(v);   //1 2 100 1000 1000 1000 1000 1000

    // 5、erase()删除容器开始迭代器中的元素
    v.erase(v.begin());
    Fun_Print(v);   //2 100 1000 1000 1000 1000 1000

    // 6、erase()删除容器开始迭代器到结束迭代器中的元素
    v.erase(v.begin(),v.end());
    Fun_Print(v);   // 

    // 7、clear()情况容器中的元素
    v.push_back(1);
    Fun_Print(v);   //1
    v.clear();
    Fun_Print(v);   // 
}

int main()
{
    test();
    cout<<endl;
    return 0;
}

//输出
1 2 3
1 2
1 2 100
1 2 100 1000 1000 1000 1000 1000
2 100 1000 1000 1000 1000 1000

1

//

6)vector数据存取

**功能:**对vector中的数据的存取操作

函数原型:

  •   at(int idx);	//返回索引idx所指的数据
    
  •   operator[i];	//返回索引idx所指的数据
    
  •   front();	//返回容器中第一个数据元素
    
  •   back();	//返回容器中最后一个数据元素
    

代码示例:

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

void Fun_Print(vector<int>& v)
{
    vector<int>::iterator it;
    for(it=v.begin() ; it!=v.end() ; it++)
        cout<<*it<<" ";
    cout<<endl;
}

void test()
{
    // 创建vector单端数组容器对象,并且通过模板参数指定容器中存放的数据的类型
    vector<int> v;

    for(int i=0;i<10;i++)
        v.push_back(i);

    // 1、at()返回索引i所指向的数据元素
    for(int i=0;i<v.size();i++)
        cout<<v.at(i)<<" ";
    cout<<endl;

    // 2、[]返回容器下标i所指向的数据元素
    for(int i=0;i<v.size();i++)
        cout<<v[i]<<" ";
    cout<<endl;

    // 3、front()返回容器中的第一个数据元素
    cout<<"v容器的第一个元素为: "<<v.front()<<endl;

    // 4、back()返回容器中的最后一个数据元素
    cout<<"v容器的最后一个元素为: "<<v.back()<<endl;
}

int main()
{
    test();
    cout<<endl;
    return 0;
}

//输出
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
v容器的第一个元素为: 0
v容器的最后一个元素为: 9

7) vector互换容器

功能描述: 实现两个容器内元素进行互换

函数原型:

swap(vec);	//将vec与本身的元素互换

代码示例:

// vector单端数组容器数据存取

#include <iostream>  // 包含标准输入输出流头文件
using namespace std;  // 使用标准命名空间

#include <vector>  // 使用vector单端数组容器,需包含头文件vector

void Fun_Print(vector<int>& v) {  // 使用引用方式&,传入vector<int>类型的形参v

	// vector<int>::iterator 拿到vector<int>这种容器的迭代器类型;每个容器都有一个专属的迭代器类型
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
		cout << *it << " ";  // it是个迭代器类型,本是是个指针,需使用*解引用
	}
	cout << endl;
}

void test() {

	// 创建vector单端数组容器对象,并且通过模板参数指定容器中存放的数据的类型
	vector<int> v;
	vector<int> v1;

	for (int i = 0; i < 10; i++) {
		// push_back()向容器尾部插入元素
		v.push_back(i);
		v1.push_back(i + 100);
	}

	cout << "容器互换前----------------------------" << endl;
	Fun_Print(v);
	Fun_Print(v1);

	v.swap(v1);  // swap()将v1容器中的数据元素与本身的数据元素进行互换

	cout << "容器互换后----------------------------" << endl;
	Fun_Print(v);
	Fun_Print(v1);
}

int main() {

	test();

	system("pause");  // 相当于在本地 Windows 调试器中的:请按任意键继续...;暂停,方便看清楚输出结果

	return 0;  // 程序正常退出
}

//输出结果
容器互换前----------------------------
0 1 2 3 4 5 6 7 8 9
100 101 102 103 104 105 106 107 108 109
容器互换后----------------------------
100 101 102 103 104 105 106 107 108 109
0 1 2 3 4 5 6 7 8 9

实际用途:使用swap收缩内存空间

// vector单端数组容器数据存取

#include <iostream>  // 包含标准输入输出流头文件
using namespace std;  // 使用标准命名空间

#include <vector>  // 使用vector单端数组容器,需包含头文件vector

void Fun_Print(vector<int>& v) {  // 使用引用方式&,传入vector<int>类型的形参v

	// vector<int>::iterator 拿到vector<int>这种容器的迭代器类型;每个容器都有一个专属的迭代器类型
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
		cout << *it << " ";  // it是个迭代器类型,本是是个指针,需使用*解引用
	}
	cout << endl;
}

void test() {

	// 创建vector单端数组容器对象,并且通过模板参数指定容器中存放的数据的类型
	vector<int> v;

	for(int i=0;i<100000 ; i++)
        v.push_back(i);

    cout<<"v容器的容量为:"<<v.capacity()<<endl;
    cout<<"v容器的个数为:"<<v.size()<<endl;

    v.resize(3);

    cout << "指定个数后------------------------------" << endl;
	cout << "v容器的容量为:" << v.capacity() << endl;  // 138255
	cout << "v容器的个数为:" << v.size() << endl;  // 3

	// 收缩内存;使用swap收缩内存
	vector<int>(v).swap(v);
	//vector<int>(v)匿名对象;相当于利用v来创建了一个新的对象,其实是调用拷贝构造函数创建一个新对象(x);
	//实际无名(x);按照v来做初始化操作,会按v目前所用的元素个数来初始化匿名对象(x)的大小;
	//所以匿名对象最开始大小和容量都为3;使用swap,容器交换后,匿名对象会进行回收


    // 互换元素的本质:相当于指针的交换
	// 匿名对象特点:当前行代码执行完后,匿名对象立即被回收,系统回收

	cout << "收缩内存后------------------------------" << endl;
	cout << "v容器的容量为:" << v.capacity() << endl;  // 3
	cout << "v容器的个数为:" << v.size() << endl;  // 3
}

int main() {

	test();

	system("pause");  // 相当于在本地 Windows 调试器中的:请按任意键继续...;暂停,方便看清楚输出结果

	return 0;  // 程序正常退出
}

//输出结果
v容器的容量为:131072
v容器的个数为:100000
指定个数后------------------------------
v容器的容量为:131072
v容器的个数为:3
收缩内存后------------------------------
v容器的容量为:3
v容器的个数为:3

8)vector预留空间

**功能描述:**减少 vector 在动态扩展容量时的扩展次数

函数原型:

reserve(int len);	//容器预留 len 个元素长度,预留位置不初始化,元素不可访问

代码示例:

// vector单端数组容器数据存取

#include <iostream>  // 包含标准输入输出流头文件
using namespace std;  // 使用标准命名空间

#include <vector>  // 使用vector单端数组容器,需包含头文件vector

void Fun_Print(vector<int>& v)    // 使用引用方式&,传入vector<int>类型的形参v
{

    // vector<int>::iterator 拿到vector<int>这种容器的迭代器类型;每个容器都有一个专属的迭代器类型
    for (vector<int>::iterator it = v.begin(); it != v.end(); it++)
    {
        cout << *it << " ";  // it是个迭代器类型,本是是个指针,需使用*解引用
    }
    cout << endl;
}

void test()
{

    // 创建vector单端数组容器对象,并且通过模板参数指定容器中存放的数据的类型
    vector<int> v;

    // 利用reserve()预留空间
    v.reserve(100);   如果一开始就预留足够的空间,就不用连续开辟(动态扩展)30次

    int num=0;  //统计开辟次数
    int* p=NULL;
    for(int i=0; i<100000 ; i++)
    {
        v.push_back(i);
        if(p!=&v[0])
        {
            p=&v[0];
            num++;
        }
    }
    cout<<"num:"<<num<<endl;
}

int main()
{

    test();

    system("pause");  // 相当于在本地 Windows 调试器中的:请按任意键继续...;暂停,方便看清楚输出结果

    return 0;  // 程序正常退出
}

//输出
num:11

10、文件操作

9.1 文件流

C++总共有输入文件流输出文件流输入输出文件流3种

  • 要打开一个输入文件流,需要定义一个 ifstream 类型的对象
  • 要打开一个输出文件流,需要定义一个**ofstream** 类型的对象
  • 要打开一个输入输出文件流,需要定义一个**fstream** 类型的对象

这3种类型都定义在头文件**< fstream >**中

9.2 文件操作

总结对文件进行操作的方法如下:

  1. 包含头文件 < fstream >

  2. 创建一个流对象

  3. 将这个流和相应的文件关联起来

    #include<iostream>
    #include<fstream>
    using namespace std;
    
    void outstream()
    {
        ofstream ofs;
        ofs.open("mytest.txt");
    }
    
    int main()
    {
        return 0;
    }
    

    因为 ifstreamofstreamfstream 这3个类都具有自动打开文件的构造函数,而这个构造函数就具有**open()**的功能

    因此在创建流对象的时候

    就可以关联文件ofstream myStream("myTest.txt");

    #include<iostream>
    #include<fstream>
    using namespace std;
    
    void outstream()
    {
        ofstream myStream("myTest.txt");
    }
    
    int main()
    {
        return 0;
    }
    
  4. 操作文件流

9.2.1 文本文件

1.写文件

注意:在写文件时,如果要打开的文件不存在或者路径错误那么会自动生成一个文件

open函数的原型如下:

void open(char const *,int filemode,int =filebuf::openprot);

有3个参数:

  • 第一个是要打开的文件名
  • 第二个是文件的打开方式
  • 第三个是文件的保护方式(一般都是用默认值)

其中第2个参数可以取下列的值:

打开方式解释
ios::in打开文件进行读操作,这种方式可以避免删除现存文件的内容
ios::out打开文件进行写操作,这是默认模式
ios::ate打开一个已有的输入或输出文件并查找到文件尾开始(at the end)
ios::app在文件尾追加方式写文件
ios::binary指定文件以二进制方式打开,默认为文本方式
ios::trunc如文件存在,将其长度截断为零并清除原有内容

这些值可以组合使用,如

  • 当你想要写入文件时从一个空文件开始,可以使用这个标志

    std::ofstream ofs("example.txt",ios::out | ios::trunc);
    
  • 以读写模式打开文件,并且如果文件已存在,其内容会被清空。如果文件不存在,将会创建一个新文件

    std::fstream fs("example.txt",ios::in | ios::out | ios::trunc);
    

例子:在文件末尾输入

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

void outprint()
{
    ofstream ofs; //打开一个写的文件流
    ofs.open("test.txt", ios::app | ios::ate);  //把这个流和文件关联起来

    ofs << "姓名:张三" << endl;
    ofs << "年龄:18" << endl;
    ofs << "性别:男" << endl;

    ofs.close();   //操作完成记得close()关闭
}

int main()
{
    outprint();
    return 0;
}

2.读文件

函数功能

函数功能
bad()如果进行非法操作,返回true,否则返回false
clear()设置内部错误状态,如果用缺省参量调用则清除所有错误位
eof()如果提取操作已经到达文件尾,则返回true,否则返回false
good()如果没有错误条件和没有设置文件结束位置,返回true,否则返回false
fail()与good相反,操作失败返回false,否则返回true
is_open()判定流对象是否成功地与文件关联,若是,返回true,否则返回false

例子:输出文件中的内容

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

void inputfile()
{
    ifstream ofs;
    ofs.open("test.txt", ios::in);
    if (ofs.is_open())
    {
        cout << "已成功和文件关联!!!" << endl;

        //方法一 遇到空格停止
        char chr[1024];
        while (ofs >> chr)
            cout << chr << endl;
        
        //方法二 可以读入空格
        char ch1[1024]={0};
    	while(ifs.getline(ch1,sizeof(ch1)))
        	cout<<ch1<<endl;
    }
    else   //不为真
        cout << "尚未与文件关联!!!" << endl;

    ofs.close();
}

int main()
{
    inputfile();
    return 0;
}

注:打开文件流完成操作后一定要关闭文件流

另外在写文件的时候使用is_open*()函数判断文件是否打开意义不大, 因为如果没有找到这个文件那么就会根据你填入的文件名自动创建一个 这样的文件无论有没有文件都会返回 true;

11、algorithm头文件

1)max()、min()、abs()函数

  • **max():**求两个数的最大值
  • **min():**求两个数的最小值
  • **abs():**求一个数的绝对值

代码示例:

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

int main()
{
    int a=-3,b=4;
    int Max=max(a,b);
    int Min=min(a,b);
    int Abs=abs(a);
    cout<<Max<<" "<<Min<<" "<<Abs<<endl;
    
    return 0;
}

//输出结果
4 3 3
    

注意:

  1. max()min() 函数中的参数只能是两个,如果想求3个数的最大值,需要嵌套一下
  2. 写了algorithm头文件后,max就变成了函数名,在自己定义变量时,要避免使用max、min等
  3. abs()函数只能用于求整型变量的绝对值#include<cmath>中的fabs()函数可以用于浮点型变量的绝对值

2)交换函数:swap()

**功能:**实现x,y值的交换

代码示例:

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

int main()
{
    int a=3,b=4;
    cout<<a<<" "<<b<<endl;
    swap(a,b);
    cout<<a<<" "<<b<<endl;
    return 0;
}

//输出
3 4
4 3

3)翻转函数:reverse()

**功能:**翻转x-y区间的数组或容器的值,区间为左闭右开,[x,y)

(1)翻转整个数组

代码示例:

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

int main()
{
    int a[5]= {11,22,33,44,55};
    for(int i=0; i<5; i++)
        cout<<a[i]<<" ";
    cout<<endl;
    reverse(a,a+5);
    for(int i=0; i<5; i++)
        cout<<a[i]<<" ";
    cout<<endl;
    return 0;
}


//输出结果
11 22 33 44 55
11 55 44 33 22

(2)也可以实现对部分值的翻转

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int main() {
	int a[5] = {11,22,33,44,55};
	reverse(a+3,a+5);
	for(int i = 0; i < 5; i++) 
		cout << a[i] << ' ';
	return 0;
 } 

//输出结果
11 22 33 55 44

(3)翻转整个容器:若想对容器中所有的数进行翻转,则需要用到begin()、end()函数

代码示例:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main()
{
    vector<int> v;
    vector<int>::iterator it;
    v.reserve(100);
    int* p;

    for(int i=0; i<5 ; i++)
    {
         v.push_back(i);
        if(p!=&v[0])
            p=&v[0];
    }

    for(int i=0;i<5;i++)
        cout<<v[i]<<" ";
    cout<<endl;

    reverse(v.begin(),v.end());
    for(int i=0;i<5;i++)
        cout<<v[i]<<" ";
    cout<<endl;

    return 0;
}

(4)翻转容器选定范围:容器的翻转也可以用迭代器,来实现指定位数的翻转

同样是左闭右开,[x,y)

代码示例:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main()
{
    vector<int> v;
    vector<int>::iterator it;
    v.reserve(100);
    int* p;

    for(int i=0; i<5 ; i++)
    {
         v.push_back(i);
        if(p!=&v[0])
            p=&v[0];
    }

    for(int i=0;i<5;i++)
        cout<<v[i]<<" ";
    cout<<endl;

    it=v.begin();
    reverse(it+1,it+3);
    for(int i=0;i<5;i++)
        cout<<v[i]<<" ";
    cout<<endl;

    return 0;
}

//输出结果
0 1 2 3 4
0 2 1 3 4

4)排序函数:sort()

(1)对x-y区间的数组、容器进行排序,默认升序排序

代码示例:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main()
{
    int a[5] = {55,44,33,22,11};

    for(int i=0;i<5;i++)
        cout<<a[i]<<" ";
    cout<<endl;

    sort(a,a+5);

    for(int i=0;i<5;i++)
        cout<<a[i]<<" ";
    cout<<endl;
    return 0;
}

//输出结果
55 44 33 22 11
11 22 33 44 55

(2)如果想将数组降序排序,就需要写一个简单的函数,改变默认的排序功能

代码示例:

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

bool cmp(int a,int b)
{
    return a>b;
}

int main()
{
    int a[5] = {11,22,33,44,55};

    for(int i=0;i<5;i++)
        cout<<a[i]<<" ";
    cout<<endl;

    sort(a,a+5,cmp);

    for(int i=0;i<5;i++)
        cout<<a[i]<<" ";
    cout<<endl;
    return 0;
}

//输出结果
11 22 33 44 55
55 44 33 22 11

(3)同理,如果对结构体排序,也需要自定义优先级

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
//用sort函数对结构体排序
struct Student {
	int high;
	int weigh;
}student[10];
//a.high如果小于b.high,则a结构体的优先级更大, 也就是说:high小的结构体排在前面。
bool cmp(Student a, Student b) {
	return a.high > b.high;
}
int main() {
	for(int i = 0; i < 10; i++) {
		student[i].high = i ;
	}

	for(int i = 0; i < 10; i++) {
		cout << student[i].high << ' ';
	}
	cout<<endl;

	sort(student, student+10, cmp);		//将自定义的函数添加上。

	for(int i = 0; i < 10; i++) {
		cout << student[i].high << ' ';
	}
	return 0;
}

//输出结果
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0

(4)如果想对容器排序,就需要使用迭代器,或begin(),end()函数

代码示例:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

bool cmp(int a,int b)
{
    return a>b;
}
int main() {
	vector<int>v;
	vector<int>::iterator it;
	//输入:
	for(int i = 0; i < 5; i++)
		v.push_back(i);
	//输出:
	for(int i = 0; i < v.size(); i++) {
		cout << v[i] << ' ';
	}
	cout<<endl;

	it = v.begin();
	sort(it, it+5,cmp);
//	sort(v.begin(), v.end())
	for(int i = 0; i < v.size(); i++) {
		cout << v[i] << ' ';
	}
	return 0;
 }

//输出结果
0 1 2 3 4
4 3 2 1 0

注意:

  1. sort()排序函数的时间复杂度大概在o(nlogn),比冒泡、简单排序等效率高
  2. reverse()函数一样,可以自由指定排序范围,也是半开半闭区间(左闭右开)

5)查找函数:find()

**功能:**查找某数组指定区间x-y内是否有x,若有,则返回该位置的地址,若没有,则返回该数组第n+1个值的地址

注意:

数组中查找是否有某值:一定一定一定要满足代码中这两个条件。

  1. p-a != 数组的长度; p是查找数值的地址,a是a[0]的地址。
  2. *p == x; 也就是该地址指向的值等于我们要查找的值。

最后输出p-a+1; p-a相当于x所在位置的地址-a[0]所在位置的地址, 但因为是从0开始算, 所以最后需要+1。

(1)对数组进行查找

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    int a[5] = {11,22,33,44,55};

    int *p = find(a,a+5,33);				//定义指针,指向查找完成后返回的地址,5为a2数组长度
    if(((p-a) != 5) && (*p == 33))		//若同时满足这两个条件,则查找成功,输出
        cout << (p-a+1);					//输出所在位置
    return 0;
}

//输出结果
3

(2)对容器进行查找

也要满足上述两个条件:

代码示例:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main() {
	vector<int>v;
	vector<int>::iterator it, it1;
	//输入: 
	for(int i = 0; i < 5; i++) 
		v.push_back(i); 
	//查找 
	int size = v.size();					//第一步:求长度
	it = find(v.begin(), v.end(), 3);		//第二步:查找x在容器的位置,返回迭代器1
	it1 = v.begin();						//第三步:令迭代器2指向容器头 
	if(((it-it1)!=size)&&(*it==3))			//第四步:若同时满足这两个条件,则查找成功,输出 
		cout << (it-it1+1) << endl;			//输出所在位置 
	return 0;
 } 

6)查找函数:upper_bound()、lower_bound()

  1. upper_bound(): 查找第一个大于x的值的位置
  2. lower_bound(): 查找第一个大于等于x的值的位置

同样是返回地址,用法和 find() 函数一样限制条件也一样

7)填充函数:fill()

功能:在区间内填充某一个值。同样适用所有类型数组,容器范围左开右闭

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    int a[5] = {11,33,22};
    for(int i = 0; i < 5; i++)
        cout << a[i] << ' ';
    cout<<endl;

    fill(a+2,a+5,9999);

    for(int i = 0; i < 5; i++)
        cout << a[i] << ' ';
    return 0;
}

//输出结果
11 33 22 0 0
11 33 9999 9999 9999

8)查找某值出现的次数:count()

(1)在数组中查找x 在某区间出现的次数:

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int main() {  
	int a[5] = {11,22,33,44,44};
	
	cout << count(a, a+5, 44);	
	
	return 0;
 } 

//输出结果
2

(2)在容器中查找同理,只是需要用iterator迭代器或begin()end()函数。

注意: 和前几个函数一样,如果需要指定区间查询,注意是半开半闭区间(左闭右开区间)

9)求最大公因数:___gcd()

另外,用二者乘积除以最大公因数即可得到最小公倍数。 因此没有求最小公倍数的函数。

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int main() {  
	int a = 12, b = 4;
	int Gcd = __gcd(a,b);
	cout << Gcd;
	return 0;
 } 

//输出结果
4

**注意: ** __gcd() 需要写两个下划线

10)求交集、并集、差集:set_intersection()、set_union()、set_difference()

(1)求交集:

  1. 将两个数组的交集赋给一个容器

    (为什么不能赋给数组呢?因为数组不能动态开辟,且inserter()函数中的参数必须是指向容器的迭代器)

    代码示例:

    #include<algorithm>
    #include<iostream>
    #include<vector>
    using namespace std;
    int main()
    {
        int a[5] = {1,2,3,4,5}, b[5] = {1,2,33,44,55};
        vector<int>::iterator it;
        vector<int>v4;
        set_intersection(a, a+5, b, b+5, inserter(v4,v4.begin()));
        for(int i = 0; i < v4.size(); i++)
        {
            cout << v4[i] << ' ';
        }
    }
    
    //输出结果
    1 2
    
  2. 将两个容器的交集赋给另一个容器:

    代码示例:

    #include<algorithm>
    #include<iostream>
    #include<vector>
    using namespace std;
    int main()
    {
        vector<int> v1, v2, v3;
        for(int i = 0; i < 5; i++)
            v1.push_back(i);
        for(int i = 3; i < 8; i++)
            v2.push_back(i);
    
        set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), inserter(v3,v3.begin()));
        for(int i = 0; i < v3.size(); i++)
        {
            cout << v3[i] << ' ';
        }
        return 0;
    }
    
    //输出结果
    3 4
    

(2)求并集:

  1. 将两个数组的并集赋给一个容器

    (为什么不能赋给数组呢?因为数组不能动态开辟,且inserter()函数中的参数必须是指向容器的迭代器)

    代码示例:

    #include<algorithm>
    #include<iostream>
    #include<vector>
    using namespace std;
    int main()
    {
        int a[5] = {1,2,3,4,5}, b[5] = {1,2,33,44,55};
        vector<int>::iterator it;
        vector<int>v4;
        set_union(a, a+5, b, b+5, inserter(v4,v4.begin()));
        for(int i = 0; i < v4.size(); i++)
        {
            cout << v4[i] << ' ';
        }
    }
    
    //输出结果
    1 2 3 4 5 33 44 55
    
  2. 将两个容器的并集赋给另一个容器

    代码示例:

    #include<algorithm>
    #include<iostream>
    #include<vector>
    using namespace std;
    int main()
    {
        vector<int> v1, v2, v3;
        for(int i = 0; i < 5; i++)
            v1.push_back(i);
        for(int i = 3; i < 8; i++)
            v2.push_back(i);
    
        set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), inserter(v3,v3.begin()));
        for(int i = 0; i < v3.size(); i++)
        {
            cout << v3[i] << ' ';
        }
        return 0;
    }
    
    //输出结果
    0 1 2 3 4 5 6 7
    
  3. 差集完全同理

注意: inserter(c,c.begin())为插入迭代器
此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

11)全排列:next_permutation()

**功能:**将给定区间的数组、容器全排列

(1)将给定区间的数组全排列

代码示例:

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    int a[3] = {1,2,3};
    do
    {
        cout<<a[0]<<a[1]<<a[2]<<endl;
    }
    while(next_permutation(a,a+3));	//输出1、2、3的全排列

    return 0;
}

//输出结果
123
132
213
231
312
321

(2)容器全排列同理:只不过将参数换成iterator迭代器或begin()end()函数。

注意: 和之前的一样,如果指定全排列区间,则该区间是半开半闭区间(左闭右开)

12、unordered_map

1. 简介

  • unordered_map是一个将key和Value关联起来的容器,可以高效的根据单个key值查找对应的Value
  • key值应该是唯一的,key和Value的数据类型可以不相同
  • unordered_map存储元素时是没有顺序的,只是根据key的哈希值,将元素存在指定位置
  • unordered_map查询单个key的时候效率比map高,但是要查询某一范围内的key值是比map效率低
  • 可以使用[]操作符来访问key值对应的Value值

2.具体应用

力扣13.罗马数字转整数

罗马数字包含以下七种字符: IVXLCDM

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II27 写做 XXVII, 即为 XX + V + II

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

  • I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
  • X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
  • C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。

示例 1:

输入: s = "III"
输出: 3

示例 2:

输入: s = "IV"
输出: 4

示例 3:

输入: s = "IX"
输出: 9

示例 4:

输入: s = "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.

示例 5:

输入: s = "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.

提示:

  • 1 <= s.length <= 15
  • s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
  • 题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999]
  • 题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。
  • IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。
  • 关于罗马数字的详尽书写规则,可以参考 罗马数字 - 百度百科

具体代码:

class Solution {
public:
    int romanToInt(string s) {
        int sum=0;
        unordered_map<char,int> LuoMaNumber = {
            {'I',1},
            {'V',5},
            {'X',10},
            {'L',50},
            {'C',100},
            {'D',500},
            {'M',1000},
        };
        for(int i=0;i<s.length();i++)
        {
            int value1=LuoMaNumber[s[i]],value2=LuoMaNumber[s[i+1]];
            if(i==s.length()-1)
            {
                sum+=value1;
                break;
            }
            if(value1<value2)
            sum-=value1;
            else sum+=value1;
        }
        return sum;
    }
};

13、Stack

Stack就是栈

使用stack需要包含头文件:#include<stack>

1. stack的定义及初始化

  • 格式: stack<数据类型> 容器名

  • 示例:

    stack<int> s1;
    stack<double> s2;
    stack<string> s3;
    stack<结构体类型> s4;
    stack<int> s5[N];	//定义一个存储数据类型为int的stack容器数组,N为大小
    

2.stack常用的成员函数

  •   empty();//判断堆栈是否为空
      pop();	//弹出堆栈顶部的元素
      push();	//向堆栈顶部添加元素
      size();	//返回堆栈中元素的个数
      top();	//返回堆栈顶部元素
    
  • 示例代码

    #include<iostream>
    #include<stack>
    using namespace std;
    
    int main()
    {
        stack<int> s;
        s.push(1);
        s.push(2);
        s.push(3);
        s.push(4);
        cout<<"堆栈中的元素个数为:"<<s.size()<<endl;
        if(s.empty())
            cout<<"堆栈为空"<<endl;
        else
            cout<<"堆栈不为空"<<endl;
        cout<<"堆栈的最顶部元素为:"<<s.top()<<endl;
        s.pop();
        cout<<"弹出最高位后堆栈的最顶部元素为:"<<s.top();
    }
    
    
    //输出结果
    堆栈中的元素个数为:4
    堆栈不为空
    堆栈的最顶部元素为:4
    弹出最高位后堆栈的最顶部元素为:3
    

for(char ch:s)

功能为遍历字符串,功能等效于

for(int i=0;i<s.length();i++)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值