C++面向对象程序设计(万字总结)

文章目录

一、c++ 语言简介

1、C++ 特点

一种编译式的、通用的、大小写敏感的编程语言,完全支持面向对象开发模式。

2、类库

输入流istream 【import stream】类的对象:cin
输出流 ostream【output stream】类的对象 :cout

istream类的对象cin用来实现基本的键盘输入。 【多个值输入,空格区分】
ostream类的对象cout用来实现基本的屏幕输出。

使用

#include <iostream>  // 标准输入输出流
using namespace std; // 命名空间

// 主函数
int main()
{
    int a, b, c;                            //定义变量a,b,c
    cin >> a >> b >> c;                     //控制台输入a,b,c 对应的值
    cout << "a + b=" << a + b << "\tc=" << c; // 控制台输出  【\t空格 \n换行】
    system("pause");
    return 0;
}

在这里插入图片描述

3、头文件

使用头文件保存程序用到的声明 (eg: 函数声明,常量定义)

1)C++语言中常用的头文件

(1)标准输入输出流:<iostream>。
(2)标准文件流:<fstream>。
(3)标准字符串处理函数:<string>。
(4)标准数学函数:<cmath >。

4、命名空间名

C++ 的一种机制,使用关键字 namespace 把大量有逻辑联系的程序实体组合在一个标识符下

1)定义格式

namespace 命名空间名{
    命名空间内的各种声明(函数声明、类声明....)
}

2)using语句有两种形式

using 命名空间名::标识符;
using namespace 命名空间名

5、强制类型装换

当不同类型的量进行混合算术运算时,系统自动进行合理的类型转换,也可以在程序中使用强制类型转换运算符 static _cast 或是 const _cast 进行转换。

使用

 1static_cast 用于将一种数据类型转换成另一种数据类型
使用格式:
static_cast<类型名>(表达式)


 2const_cast 用于去除指针和引用的常量性,但不能去除变量的常量性。
 使用格式:
const_cast<类型名>(表达式)

6、引用 (& 引用名)

函数名不能有地址取地址符

类型名 &引用名 = 同类型的某变量名;

eg:
int oneInt;
int &aname = oneInt;  //声明引用

注:声明引用后,系统并不为aname 分配空间,aname 与oneInt对应的是同一个内存地址

1)普通引用

修改了变量 (oneInt)的值,引用值 (aname)也会改变
修改了引用值 (aname),变量 (oneInt)的值也会改变

int oneInt;
int &aname = oneInt; //声明引用aname 类型是int &

2)常引用

修改了变量 (oneInt)的值,引用值 (cname )也会改变
不能修改cname 的值 【不能使用常引用对所引用的变量进行修改】

int oneInt;
const int &cname = oneInt;  //定义了常引用cname 类型是const int &
// cname = 2 //错误

3)引用在函数中

在C++语言中,函数调用时参数有两种传递方式:普通传参和引用传参。
传值:实际上传递的是对象的值
传引用:是传递对象的首地址值

普通传参:

函数传过来的内容,在函数里的内容(形参)改变,在函数外定义的内容【地址】(实参)也会改变

引用传参:

函数传过来的内容,在函数里的内容(形参)改变,在函数外定义的内容【地址】(实参)不会改变

#include <iostream>
using namespace std;
void SwapValue(int a, int b)  // 
{
    int tmp;
    tmp = a;
    a = b;   // 20
    b = tmp; // 10
    cout << "SwapValue函数中a=" << a << ",b=" << b << endl;
    return;
}
void SwapRef(int &a, int &b)
{
    int tmp;
    tmp = a;
    a = b;   // 20
    b = tmp; // 10
    cout << "SwapRef函数中&a=" << a << ",&b=" << b << endl;
    return;
}
int main()
{
    int a = 10;
    int b = 20;
    SwapValue(a, b);
    cout << "SwapValue函数后a=" << a << ",b=" << b << endl;
    // 更换值
    a = 10;
    b = 20;
    SwapRef(a, b);
    cout << "SwapRef函数后&a=" << a << ",&b=" << b << endl;
    system("pause");
    return 0;
}

在这里插入图片描述

4)引用作为函数返回值

#include <iostream>
using namespace std;
int a = 30;
int &refValue(int &x)
{
    return x;
}
int main()
{
    cout << "原来的a值:" << a << endl;
    cout << refValue(a) << endl;
    refValue(a) = 40; // 把40赋值给a 类型是int &
    cout << "a=" << a << endl;
    system("pause");
    return 0;
}

在这里插入图片描述

7、指针和动态内存分配

1)指针指向 (* 指针名)

#include <iostream>
using namespace std;
int main()
{
    int a = 10, *p = &a;  // 声明指针p 指向整型 a (地址)【p中保存a的地址,不是a的值】
    cout << "原来指针地址:" << *p << endl;
    a = 20; // a 地址的值为20
    cout << "更换地址&a的值 指针地址:" << *p << endl;
    system("pause");
    return 0;
}

在这里插入图片描述

2)动态内存分配

“动态内存分配”机制:在程序运行期间,根据实际需要,临时分配一段内存空间用于存储数据。与声明数组时就指定数组大小的内存分配方法不同,这种内存分配是在程序运行期间进行的,故称为“动态内存分配”。
相对地,编译时确定数组空间大小的方式可以称为“静态内存分配”。静态内存分配在编译时就能确定占用内存的大小,而动态内存分配一定是在运行期间才能确定占用内存的大小。

        // new运算符实现动态内存分配
p=new T;  //T是任意类型名,p是类型T*的指针 ,内存空间为sizeof(T)字节

3)动态分配整型数组

#include <iostream>
using namespace std;
int main()
{
    int *pArray; // 定义数组指针
    int i = 5;
    pArray = new int[i * 20];  // 占四个字节大小的动态内存空间
    cout << "pArray指针指向长度:" << sizeof(pArray) << endl; //指针指向长度
    pArray[0] = 9;                                           // 数组的第一个值为90
    pArray[1] = 90;                                           // 数组的第二个值为90
    pArray[109] = 90;                                           // 数组的第110个值为90 (越界)
    cout << "pArray:" << pArray << endl;
    cout << "pArray[100] 越界:" << pArray[100] << endl;
    cout << "pArray指针释放前获取数组值:" << pArray[1] << endl;
    delete []pArray; //释放数组指针 普通释放 delete 指针;
    cout << "pArray指针释放后获取不到数组的值:" << pArray[1] << endl;
    system("pause");
    return 0;
}

在这里插入图片描述

4)释放空间 (delete )

注 :使用new 运算符动态申请的内存空间,需要在使用完毕释放。

C++提供了 delete运集符,用来释放动态分配的内存空间。
delete运算符的基本用法如下:

delete 指针;

delete []指针;  //释放数组空间

delete运算符后面的指针必须是指向动态分配的内存空间的,否则运行时很可能会错。

int oneInt=6;
int *p=&ontInt;
couts<*p<<endl;
delete p; //出错,p是引用,不是动态分配的

int *q=new int;
*q=8;
cout<<*q<<endl;
delete q; //正确,q指向动态分配的空间

注:使用new 运算符动态分配的内存空间,一定要用 delete 运算符释放。否则,即使程序行结束,这部分内存空间仍然不会被操作系统吸回,从而成为被白白浪费掉的内存垃圾。种现象称为“内存泄露”。

7、string对象处理字符串

C++标准模板库中提供了 string 数据类型,专门用于处理字符串。string是一个类,这个类型的变量称为“string 对象”。

语句:

#include <string>

声明string 对象

string 变量名; 

## eg:
string str1;  //声明string 对象 str1,值为空
string str2 = "rong";  //声明string 对象 str2, 并使用字符串常量进行初始化 (str2 值为rong)

string 类中常用的成员函数

函数功能
const char *c_str() const;返回一个指向字符串的指针,字符串内容与本string串相同,用于将string 转换为 const char *
int size() const;返回当前字符串的大小
int length() const;返回当前字符串的长度
bool empty() const;判定当前字符串是否为空
size_type find( const char *str, size_type index );返回 str 在字符串中第一次出现的位置(从 index 开始查找),如果没找到则返回-1
size_type find( char ch, size_type index );返回字符ch在字符串中第一次出现的位置(从index开始查找),如果没找到则返回-1
string &insert(int p,const string &s);在p位置插入字符串s
string &append(const char *s);将字符串s连接到当前字符串的结尾处
string substr(int pos=0,int n= npos) const;返回从pos开始的n个字符组成的字符串

例子

#include <iostream>
#include <string.h>
#include <cstring> //可以使用 strcpy() 复制 的string成员函数
using namespace std;
int main()
{
    char name[] = "OCEANSIDE";
    string str1 = name; // 声明string 对象 str1
    cout << "字符串数组对string变量进行初始化:" << str1 << endl;
    // string 对象数组
    string Rong[] = {"OCEANSIDE","泰语小分队","圣诞老人"};
    cout << "对象数组下标值:" << Rong[1] << endl;
    // sizeof(Rong)整个数组占用的空间大小, sizeof(string)每个string对象的大小
    cout << "数组元素个数:" << sizeof(Rong) / sizeof(string) << endl;
    string s2 = "天线本宝宝";
    string s3;
    s3 = s2;
    s3 += "baby";
    cout << "s3:" << s3 << endl;
    string s4;
    if (s4.empty()) //判断字符
        cout << "当前字符串是空的" << endl;
    s4 = s4.append("小蓉蓉"); //末尾添加
    cout << "字符串大小:" << s4.size() << "\n"
         << "字符串长度:" << s4.length() << endl;
    cout << "find:" << s4.find("蓉", 2) << endl;
    cout << "插入内容:" << s4.insert(6, "呐") << endl; //在2的位置插入 呐 【每个汉字长度是2】
    char s5[20];
    strcpy(s5, s4.c_str()); // 将s4 的内容复制到s5内
    cout << "复制内容:" << s5 << endl;
    system("pause");
    return 0;
}

在这里插入图片描述

8、c++ 语言的程序结构

C++程序以.cpp 作为文件扩展名,文件中包含若干个类和若干个函数。程序中必须有且仅有一个主函数main(),这是程序的执行的总入口。主函数也称主程序。程序从主函数的开始处执行,按照其控制结构,一直执行到结束。

程序的结束。

1)在主函数中遇到return语句。
2)执行到主函数最后面的括号}。
主函数中可以调用程序中定义的其他函数,但其他函数不能调用主函数。主函数仅是系统为执行程序时所调用的。
return 语句的功能:使主函数main结束并将相应的数值返回给运行程序的操作系统。

注释

1)从开始,到/结束,这之间的所有内容都视作注释。
2)从/直到行尾,都是注释。

9、练习

pow() 函数

pow() 函数返回第⼀个参数的第⼆个参数次⽅的结果。该函数在cmath 头⽂件中定义。
在 C++ 中,pow(x, y) 【double pow( double x, double y );】计算x的y次幂。

#include <iostream>

#include <string.h>

#include <cmath>

using namespace std;

int f(int);

int f(int a)
{
 int b = 0, c = 1;
 b++;
 c++;
 cout << "pow值:" << pow(4, 2) << endl;
 return int(a + pow(double(b), 2) + c);
}

int main()
{
 int i;
 for (i = 0; i < 3; i++)
 cout << f(i) << endl;
 system("pause");
 return 0;
}

在这里插入图片描述

二、面向对象

1、结构化程序设计

强调的是数据类型和程序结构,注重程序的易读性、可靠性及可维护性。也称为面向过程的设计方法。
程序基本都含有顺序、选择和循环三种基本控制结构。

数据结构+算法=程序

2、面向对象程序设计

1)概念

使分析、设计和实现一个系统的方法尽可能地接近人们认识一个系统的方法。
包括面向对象的分析、面向对象的设计、和面向对象的程序设计三个方面。
对象的两个特性:状态【对象本身的信息】和属性【对象的操作】
对象描述:对象名、属性和操作三要素
描述属性的数据称为成员变量或数据成员,函数称为成员函数。

2)特点

抽象、封装、继承和多态

抽象

在面向对象程序设计中,将一组数据和这组数据有关的操作集合【同一事物的共同的特点】组装在一起形成对象,这个过程叫抽象。

封装

封装就是把对象的属性和操作结合成一个独立的系统单位,并尽可能隐蔽对象的内部细节。C++语言通过建立用户定义的类来支持数据封装和信息隐藏。【每个给定类的对象包含了这个类所规定的若干是有成员、公有成员及保护成员】

继承

继承是面向对象程序设计的一个重要特性,是软件复用的一种形式,它允许在原有类的基础上创建新的【编写一个"新"类的时候,已现有的类作为基础,使得新类从现有的类“派生”而来,从而达到代码扩充和代码的复用】

Tip:
1)基类:在面向对象设计中,被定义为包含所有实体共性的class类型,被称为“基类”。也称父类或超类
2)派生类:现有的类的基础上声明新的类,将一个已有的类的数据和函数保留,并加上自己特殊的数据和函数【继承和复用的思想】也称子类

多态

多态是指不同种类的对象都具有名称相同的行为,而具体行为的实现方式却有所不同。

3、类

类是用户自定义的数据类型。对具有相同属性和行为的同一类对象的描述,其内部包括属性(本类的成员变量)和行为(本类的成员函数)两个主要部分。

Tip:基本数据类型
bool(布尔类型)、char(字符型)、int(整型)、float(浮点型)、double(双精度浮点型)

1)格式

声明类时:
分配空间是定义,没分配空间的是声明。给数据赋值是定义的操作

class 类名{
   访问范围说明符: 
       成员变量1
       成员变量2
       ...
       成员函数1
       成员函数2
       ...
    访问范围说明符:
       更多变量
}

2)访问说明符

public(公有)、private(私有)、protected(保护)

1)成员的访问范围由它之前最近的说明符决定
2)三种关键字出现的先后次序没有限制
3)若没有说明是什么访问范围,则默认为私有(private)

私有类型的成员在类外是不能访问的,可以通过公有函数访问。也可以通过友元函数【friend】访问。只有在类内和在友元函数内才可以访问私有成员。
设置私有成员的机制叫作“隐藏”

3)虚基类

虚基类 (virtual base class) 是用关键字 virtual 声明继承的父类.
我们希望继承间接共同基类时只保留一份成员, 所以虚基类就诞生了. 当基类通过多条派生路径被一个派生类继承时, 该派生类只继承该基类一次.

目的

消除多重继承的二义性,菱形继承在两个基类拥有相同的方法,派生类调用该方法出现二义性

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

eg:
// N 类
class N {
public:
    int n;
    N(int n) : n(n) {};
};
//A 类: 
class A : virtual public N {
public:
    A(int n) : N(n) {};
};

总结
● 使用多重继承时要十分小心, 否则会进场出现二义性问题
● 不提倡在程序中使用多重继承
● 只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承
● 能用单一继承解决的问题就不要使用多重继承

4)内联函数

成员函数在类体内部定义,默认是内联函数。
成员函数在类体内部声明函数,并加上inline关键字,在类体外给出函数定义。【类体内必须要有函数原型,类体外函数定义前面必须用“类名::”来限定】

Tip:
符号 :: 是类作用域运算符,表明它后面的成员函数是属于类名标识的这个类

格式:

返回值类型 类名::成员函数名(参数列表){
    函数体
}

## eg:
// 类
class nameClass{
    public:
       int fuName(int); //成员函数公有,以便外界和这个类的对象相互作用的接口
    private:
       int name;  //成员变量私有,以便隐藏数据
}
//类体外
int nameClass::fuName(int name){
  ...
}

4、对象

具有类类型的变量。完整定义了类后,即可创建对象【声明、定义或者为生成对象】

Tip:定义类数据类型只是告诉编译系统该数据类型的结构和框架,并没有分配内存。类只是一个样板,根据这个样板,可以在内存中开辟出同样结构的实例,即对象。对象是类的实例。
创建对象就是为对象分配内存,按照类定义声明的成员变量为对象分配内存。

1)格式

1)
   类名 对象名();
    # OR
   类名 对象名(参数);
    # OR
   类名 对象名 = 类名(参数);
## 多个对象
类名 对象名1,对象名2,...;
    # OR
   类名 对象名1(参数1),对象名2(参数2),...;
2)
   类名 *对象指针名() = new 类名;  // 创建对象时,调用无参的构造函数,类中成员变量不进行初始化
    # OR
   类名 *对象指针名() = new 类名(); // 创建对象时,调用无参的构造函数,类中成员变量进行初始化
    # OR
   类名 *对象指针名() = new 类名(参数);
3)声明对象引用
  类名 &对象引用名 = 对象;
4)声明对象指针
  类名 *对象指针名 = 对象地址;
5)声明对象数组
  类名 对象数组名[数组大小];

Tip:同类型的对象之间可以相互赋值,对象和对象指针都可以用作函数参数,函数的返回值可以是对象或指向对象的指针。

Tip:用new创建对象时返回的是一个对象指针,这个指针指向本类刚创建的这个对象。C++分配给指针的仅仅是存储指针值的空间,而对象所占用的空间分配在堆上。使用new 创建的对象,必须用delete来撤销【释放指针】。

2)访问对象的成员

A)对象访问
1)访问成员变量
对象名.成员变量名

2)调用成员函数
对象名.成员函数(参数表)

C )指针访问对象的成员 ->

指针->成员名

Student *sp = &st; //声明 指针对象(Student类)
sp->setName(); // 访问

Tip:sp = &st 不在声明 sp 的同时初始化 ,直接赋值

D)使用引用访问对象的成员
Student &sy = st; //声明 引用对象 (Student类)
sy.setName();  // 访问

代码例子:
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
    string name = "泰语小分队"; //成员变量
    void setName(string);       //成员函数
    void setAge(int);
    int getAge();

private:
    int age; // 私有成员变量
};
void Student::setName(string n)
{
    name = n; //设置名字
    return;
}
// 设置年龄
void Student::setAge(int a)
{
    age = a;
    return;
}
// 获取年龄
int Student::getAge()
{
    cout << "您输入年龄是:" << age << endl;
    return age;
}
int main()
{
    Student st; // Student类  st对象
     // A、B)对象访问的成员
    cout << "访问原来对象成员变量:" << st.name << endl;
    st.setName("小蓉蓉"); //调用问对象的成员函数
    cout << "访问更换对象的成员变量:" << st.name << endl;
    int a;
    // C )使用指针访问对象的成员
    Student *sp = &st; //指向 st对象的 指针sp  【sp = &st 直接赋值】
    cin >> a;          //输入年龄
    sp->setAge(a);     //指针调用对象成员(调用类Student 的setAge函数)
    sp->getAge();
    // D)使用引用访问对象的成员
    Student &sy = st; // st 的别名 sy
    cin >> a;
    sy.setAge(a); // 引用的使用和普通的对象使用成员一样
    sy.getAge();
    return 0;
}

程序结果

访问原来对象成员变量:泰语小分队
访问更换对象的成员变量:小蓉蓉
18
您输入年龄是:18
20
您输入年龄是:20
请按任意键继续. . .

5、标识符的作用域与可见性

标识符:类名、函数名、变量名、常量名和枚举类型的取值等。
标识符的作用域:标识符的有效范围。

1)函数原型作用域

C++中最小的作用域

// 标识符age 的作用域范围在函数 function 形参列表的左右括号之间
int function(int age);  

2)局部作用域

块:程序中使用相匹配的一大对大括号括起来的一段程序。
局部作用域:在块中作用域。

void function(int a)
{
    int b = a;
    //下面的都是b的作用域
    if (b > 0)
    {
        int c;
        // c 的作用域
        cin >> c;
        cout << b << endl;
    }
}


3)类作用域

类,可以看做是一组有名字的成员集合,类A的成员m 具有类作用域。

A)类内直接访问成员
B)类外a.m 或者 A::m 【a是类A的对象】
C)类外指针 pa->m 【pa是指向类A的一个对象指针】

4)命名空间作用域

一个命名空间确定一个命名空间左右域,只要是在该命名空间声明的,不属于前面所举例的作用域标识符,都属于该命名空间作用域。

// 定义命名空间 空间模块
namespace 命名空间名{
    命名空间的声明(函数声明、类声明...}
// 使用
命名空间::标识符名

## eg:
//定义
namespace nameNs{
  class aClass{...};
  aFunction(int a){...}
};
//使用
1)命名空间限定
nameNs::aClass obj; //声明一个nameNs::aClass型的对象obj
nameNs::aFunction(2); // 调用aFunction 函数
2using
using 命名空间名::标识符名;
using namespace 命名空间名; // using namespace std;

三、类和对象

1、构造函数

构造函数:类中的特殊成员函数,它属于类的一部分。在生成对象时,系统自动调用构造函数,用户在程序中不会直接调用构造函数。

1)定义

构造函数的函数名与类名相同,没有返回值。一个类的构造函数可以有多个,即构造函数的重载。

在类中没有定义任何构造函数时,系统会自动添加一个参数列表为空、函数体也为空的构造函数,称为默认构造函数,所以任何类都可以保证至少有一个构造函数。

Tip:在声明类的构造函数时可以同时给出函数体,这样的构造函数称为内联函数。

2)格式

A)类体内
类名(形参1,形参2...)

B)类体外
1)
类名::类名(形参1,形参2...)
2)
类名::类名(形参1,形参2...){
  x1=形参1;
  ...
}
3)
类名::类名(){
  x1= 初始化表达式;
  ...
}

3)作用

构造函数是用来为对象进行初始化,所以在构造函数中主要的工作是显示的为成员变量赋值。当程序创建一个对象时,系统自动调用构造函数来初始化该对象。

Tip : 对象需要占据内存空间,生成对象时,为对象分配的这段内存空间的初始化由构造函数完成。

4)复制构造函数

使用一个已存在的对象去初始化另一个正在创建的对象,只有一个参数,参数类型是本类的引用

声明和实现
class 类名{
   public:
      类名(形参表);  // 构造函数
      类名(类名 & 对象名);  // 复制构造函数
}

类名::类名(类名 & 对象名){  // 复制构造函数的实现
   函数体
}
    

自动调用复制构造函数

1)一个对象去初始化另一个对象
类名 对象名2(对象名1);
类名 对象名2 = 对象名1;
2)函数Fn 的参数是 对象
function (对象1){};
3)函数返回值是对象
function (){
return 对象1;
}

5)类型装换构造函数

构造函数只有一个参数,则可以看作是类型装换的构造函数。

6)代码实例

#include <iostream>
#include <string>
using namespace std;
class Worker
{
public:
    Worker();                //构造函数
    Worker(string);          //带参构造函数
    Worker(const Worker &p); // 复制构造函数声明
    void setName(string);    //成员函数
    void setAge(int);
    int getAge();
    // 成员函数
    void printWorker()
    {
        cout << "昵称:" << name << "\n年龄:" << age << endl;
    }

private:
    int age;                    // 私有成员变量
    string name = "泰语小分队"; //成员变量
};
Worker::Worker() : name(){};          //不带参数
Worker::Worker(string n) : name(n){}; //不带参数
Worker::Worker(const Worker &p)  // 复制构造函数实现
{
    name = "copy:" + p.name; // 获取对象的成员
    age = p.age;
}
void Worker::setName(string n)
{
    name = n; //设置名字
    return;
}
// 设置年龄
void Worker::setAge(int a)
{
    age = a;
    return;
}
// 获取年龄
int Worker::getAge()
{
    cout << "您输入年龄是:" << age << endl;
    return age;
}
int main()
{
    Worker wk; // Worker类  wk对象 【默认不带参数的】
    int age;
    cin >> age;
    // 年龄输入和获取
    wk.setAge(age);
    wk.getAge();
    wk.printWorker();
    Worker wkn("OCEANSIDE"); //  带参数的构造函数
    wkn.printWorker();       // 执行带参数的构造函数
    Worker wkCopy(wkn);      // 调用复制函数 【新建一个对象,将其他对象单做参数传个复制构造函数】
    wkCopy.printWorker();    // 复制函数调用对象的成员
    return 0;
}

程序结果:

18
您输入年龄是:18
昵称:
年龄:18
昵称:OCEANSIDE
年龄:0
昵称:copy:OCEANSIDE
年龄:0
请按任意键继续. . .

2、析构函数

1)概念

  • 析构函数:与构造函数功能相反,析构函数是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
  • 析构函数用来完成对象资源清理的工作【】

1)析构函数名是在类名前加上字符 ~。
2)无参数无返回值。
3)一个类有且只有一个析构函数。若用户没有显式定义,系统会自动生成默认的析构函数。【默认西沟函数的函数体为空】
4)当对象生命周期结束时,C++编译系统系统会自动调用析构函数
5)使用delete删除,首先为这个动态对象调用本类的析构函数,然后再释放这个动态对象占用的内存。

class MyClass{
  public:
    ~MyClass(){
        cout<<"析构函数"<<endl;
    }
}

3、类的静态成员(static)

根据定义的位置分为静态全局变量和静态局部变量。静态变量均存储在全局数据区。
类的静态成员:可分为静态成员变量和静态成员函数。静态成员变量本质上是全局变量。
未给初始值,则默认为 0 。

静态成员函数与一般成员函数的区别:
1)可以不指向某个具体的对象,只与类名连用。
2)在没有建立对象之前,静态成员就已经存在。
3)静态成员是类的成员,不是对象的成员。
4)静态成员为该类的所有对象共享,他们被存储在一个公用的内存中。
5)没有this指针,除非显示的把指针传给他们。

1)静态全局变量:

static 修饰的全局变量。
全局变量:在花括号之外声明的变量,其作用域范围是全局可见,整个项目文件都有效。

2)静态局部变量:

static 修饰的局部变量。
块内定义的变量,只执行一次初始化。

Tip:静态函数中,不能直接调用非静态函数。

4、变量及对象的生存周期和作用域

1)变量

变量生存周期:变量所占据的内存空间由分配到释放的时期。
作用域:全局域和局部域。

生存周期

变量所占据的内存空间由分配到释放的时期。

作用域

A)全局域
程序作用域和文件作用域

  • 程序作用域:多文件作用域,可通过extern 关键字进行说明的外部变量及外部函数
  • 文件作用域:单文件作用域。定义在所有函数之外的标识符具有文件作用域,从定义处开始,到整个源文件结束。

B)局部域
类作用域、函数作用域、块作用域和函数原型作用域

  • 类作用域:定义的那一个类的类体内
  • 函数作用域:函数的函数体内
  • 块:大括号括起来的程序段
  • 函数原型:作用域结束于参数表的右括号。【原型不是定义函数,而仅仅是声明函数】

2)对象

生存周期

类的对象在生成时调用构造函数,在消亡时调用析构函数,在这个两个函数调用之间则是对象的生存周期。

5、常量成员和常引用成员

常量:使用 const 修饰的量

常成员

Tip:在对象被创建后,其常量成员变量的值就不允许被修改,只可以读取其值。常量成员变量的值不能被修改,常量对象中的各个属性值均不能修改。

常量对象和常量成员变量
const 数据类型 常量名 = 表达式;

常量函数
类型说明符 函数名 (参数列表) const;

eg:
#include <iostream>
#include <string>
using namespace std;
class CDemo
{
public:
    void printValue()
    {
        cout << "非常量成员函数" << endl;
    }
    void printVcon() const
    {
        cout << "常量成员函数" << endl;
    }
};
int main()
{
    const CDemo Objcon; // Obj 是常量对象 【只能访问常量成员】
    CDemo Obj;          // 普通对象
    Objcon.printVcon(); //常量对象访问常量成员函数
    Obj.printValue();   //非常量对象访问非常量成员函数
    Obj.printVcon();   //非常量对象访问常量成员函数
    return 0;
}

程序输出

常量成员函数
非常量成员函数
常量成员函数
请按任意键继续. . .

常引用

const 数据类型 &常量名;

eg:

#include <iostream>
#include <string>
using namespace std;
class CDemo
{
public:
    const int num;  //常量型成员变量
    const int &ref; // 常量引用型成员变量 【不可修改】
    int value;

public:
    // 构造函数及初始化
    CDemo(int n) : num(n), ref(value), value(8){};
};
int main()
{
    cout << sizeof(CDemo) << endl;
    CDemo cd(23);
    // cd.ref = cd.value; //错误 【常引用不可修改】
    cout << "cd.num:" << cd.num << "\ncd.ref:" << cd.ref << "\ncd.value:" << cd.value << endl;
    return 0;
}

程序结果

24
cd.num:23
cd.ref:8
cd.value:8
请按任意键继续. . .

6、成员对象和封闭类

1)成员对象

一个类的成员变量是另一个类的对象,则该成员变量称为成员对象。

2)封闭类

包含成员对象的类。

定义类Worker 和 类 Company,Company包含成员对象类 Worker ,或者在类Company中定义一个函数,返回值是类Worker,则类Company包含 类 Worker,类Worker 是封闭类。
Tip:
执行封闭类的构造函数时,先执行成员对象的构造函数,再执行本类的构造函数。
当封闭类消亡时,先执行封闭类的析构函数,再执行成员对象的析构函数。

代码eg:

#include <iostream>
#include <string>
using namespace std;
// 工作者类
class Worker
{
public:
    string position; // 成员变量
    string name;

public:
    Worker() : position("程序员"), name("小蓉蓉"){};     //无参构造函数并初始化
    Worker(string p, string n) : position(p), name(n){}; //带参构造函数并初始化
    // 输出内容的成员函数
    void printWorker()
    {
        cout << "Worker类:"
             << "职位:" << position << ",姓名:" << name << endl;
    };
};
// 公司类
class Company
{
public:
    string address;
    Worker wk; //声明成员对象
public:
    Company()
    {
        address = "广州";
        Worker(); // 类Company包含Worker
    };
    Company(string adr, string p, string n) : address(adr), wk(p, n){}; // wk 为Worker 对象,并传position和name 参数
    // 获取地址 (函数返回当前类的地址参数)
    string getAddress()
    {
        return address;
    }
    // 成员函数返回另一个类Worker类的对象
    Worker getWorker()
    {
        return wk;
    }
};
int main()
{
    // 无参数 【获取各自的默认值】
    Company com;
    cout << "Company类:地址:" << com.getAddress() << ","; //"广州"
    com.getWorker().printWorker();                          //获取成员对象默认的内容
    // 有参数
    Company com1("韶关", "泰语翻译", "泰语小分队");
    cout << "Company类:地址:" << com1.getAddress() << ","; //"韶关"
    com1.getWorker().printWorker();                          //获取成员对象填写的内容
    return 0;
}

程序结果:

Company类:地址:广州,Worker类:职位:程序员,姓名:小蓉蓉
Company类:地址:韶关,Worker类:职位:泰语翻译,姓名:泰语小分队
请按任意键继续. . .

7、友元函数(friend)

1)友元

Tip:友元实际上并不是面向对象的特征,而是为了兼顾C语言程序设计的习惯与C++信息隐藏【私有private】的特点。破坏了类的封装和信息隐藏,但有助于数据共享,能够提高程序执行的效率。

私有成员对于类外部的所有程序部分而言都是隐藏的,访问它们需要调用一个公共成员函数,但有时也可能会需要创建该规则的一项例外。

友元使用关键字 friend 标识。

2)友元函数

友元函数是一个不属于类成员的函数,但它可以访问该类的私有成员。换句话说,友元函数被视为好像是该类的一个成员。友元函数可以是常规的独立函数,也可以是其他类的成员。实际上,整个类都可以声明为另一个类的友元。

声明
//将一个全局函数声明为本类的友元函数
friend 返回值类型 函数名(参数列表);

// 将类A 的成员函数说明为本类友元函数
friend 返回值类型 类A::类A的成员函数名(参数列表);

不能把其他类的私有成员函数声明为友元函数。
友元函数不是类的成员变量,但允许访问类中的所有成员。【函数体中访问成员:对象名.对象成员名】
友元函数不受类中的访问权限关键字限制,公有(public)、私有(private)、保护(provide)结果都是一样的。

3)友元类

将一个类 B 说明为另一个类A的友元类,则类 B 中的所有函数都是类A的友元函数,在类B的所有成员函数中都可以访问类A中的所有成员。

格式
friend class 类名;

Tip:友元类关系是单向的,若类B 是类 A 的友元类,不等于类A也是类B的友元类。友元关系也不可以传递,若类B 是类 A 的友元类,类C是类B的友元类,不等于类C是类A的友元类。

4)代码例子:

#include <iostream>
#include <string>
using namespace std;
class FriedClass;
class Test
{
public:
    void FriedClassT(FriedClass f);
};
class FriedClass
{
private:
    int x = 0, y = 2;

public:
    FriedClass(int x1, int y1)
    {
        x = x1;
        y = y1;
    }
    void printxy()
    {
        cout << "FriedClass:x=" << x << ",y=" << y << endl;
    }
    friend void coutValue(FriedClass p, FriedClass p1)
    { // 友元函数(原型),全局函数
        cout << "coutValue:x=" << p.x + p1.x << ",y=" << p.y + p1.y << endl;
    }
    friend void Test::FriedClassT(FriedClass f); // 类Test 的成员函数FriedClassT为友元函数
};
void Test::FriedClassT(FriedClass f)
{
    cout << "Test:x=" << f.x << ",y=" << f.y << endl;
    return;
}
int main()
{
    FriedClass p(1, 2), p1(3, 4);
    p.printxy();
    Test t;
    t.FriedClassT(p); //通过对象调用类的成员函数
    coutValue(p, p1); // 全局可直接调用
    return 0;
}

程序输出

FriedClass:x=1,y=2
Test:x=1,y=2
coutValue:x=4,y=6
请按任意键继续. . .

5)友元使用复数 【友元类】

友元类
#include <iostream>
#include <string>
using namespace std;
class myComplex //复数类
{
private:
    double real, imag; // 复数的实部和虚部

public:
    myComplex();
    myComplex(double r, double i);
    friend class oper; // 友元类
};
myComplex::myComplex()
{
    real = 0;
    imag = 0;
}
myComplex::myComplex(double r, double i)
{
    real = r;
    imag = i;
}
class oper
{
public:
    myComplex addCom(myComplex c1, myComplex c2); // 成员函数
    void outCom(myComplex c)                      // 成员函数
    {
        cout << "(real:" << c.real << ",imag:" << c.imag << ")" << endl;
    }
};
myComplex oper::addCom(myComplex c1, myComplex c2)
{
    return myComplex(c1.real + c2.real, c1.imag + c2.imag);
}
int main()
{
    myComplex c1(1, 2), c2(3, 4), res; // 三个对象【c1,c2,res】
    oper o;                            // 类oper 为myComplex的友元类,所以可以访问myComplex类的所有成员
    res = o.addCom(c1, c2);            // 调用oper 类的addCom成员函数,函数的参数是myComplex类里面的。
    o.outCom(c1);                      //调用oper 类的输出的友元函数
    cout << "+";
    o.outCom(c2);
    cout << "=";
    o.outCom(res); // addCom(c1, c2)
    return 0;
}

程序输出:

(real:1,imag:2)
+(real:3,imag:4)
=(real:4,imag:6)
请按任意键继续. . .

普通
#include <iostream>
#include <string>
using namespace std;
class myComplex //复数类
{
private:
    double real, imag; // 复数的实部和虚部

public:
    myComplex();
    myComplex(double r, double i);
    friend myComplex addCom(myComplex c1, myComplex c2); // 友元函数  (两个类myComplex的参数对象)
    friend void outCom(myComplex c)                      // 友元函数
    {
        cout << "(real:" << c.real << ",imag:" << c.imag << ")" << endl;
    }
};
myComplex::myComplex()
{
    real = 0;
    imag = 0;
}
myComplex::myComplex(double r, double i)
{
    real = r;
    imag = i;
}
myComplex addCom(myComplex c1, myComplex c2)
{
    return myComplex(c1.real + c2.real, c1.imag + c2.imag);
}
int main()
{
    myComplex c1(1, 2), c2(3, 4), res; // 三个对象【c1,c2,res】
    res = addCom(c1, c2);              // 调用友元函数不通过类对象
    outCom(c1);                        //调用输出的友元函数
    cout << "+";
    outCom(c2);
    cout << "=";
    outCom(res); // addCom(c1, c2)
    return 0;
}

程序输出:

(real:1,imag:2)
+(real:3,imag:4)
=(real:4,imag:6)
请按任意键继续. . .

8、this指针

this 指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数联系在一起。

this指针:当调用一个成员函数时,系统自动向它传递一个隐含的参数,该参数是一个指向调用该函数的对象指针。
对于非静态成员函数,这个this指针指向了成员函数作用的对象。【静态成员是类具有的属性,不是对象的特征,this表示的是隐藏的对象的指针,所以静态成员没有this 指针】

#include <iostream>
#include <string>
using namespace std;
class myComplex //复数类
{
private:
    double real, imag; // 复数的实部和虚部

public:
    myComplex():real(0),imag(0){};
    myComplex(double r, double i)
    {
        this->real = r; // 对象内容赋值
        this->imag = i;
    };
    myComplex AddImag()
    {
        this->imag++;
        return *this; //返回对象本身
    }
    void outCom()
    {
        cout << "(real:" << real << ",imag:" << imag << ")" << endl;
    }
};
int main()
{
    myComplex c(2, 3), c2;
    c.outCom();
    cout << "执行AddImag" << endl;
    c2 = c.AddImag(); // c 是传入函数的对象
    c.outCom();
    system("pause");
    return 0;
}

程序执行结果:

(real:2,imag:3)
执行AddImag
(real:2,imag:4)
请按任意键继续. . .

四、运算符重载

1、运算符重载概念

1)运算符重载:

给已有的运算符赋予多重含义,使用一个运算符作用于不同类型的数据时产生不同的行为。

目的:

使C++中的运算符也能用来操作对象

实质:

编写以运算符为名称的函数,使用运算符的表达式为对重载函数的调用

格式:
返回值类型 operator 运算符(参数){
   函数体
}

eg:

传参

运算符可以被重载为全局函数,也可以被重载为类的成员函数。
定义为全局函数:
函数的参数个数就是运算符的操作个数,运算符的操作数就成为函数的实参。
1)一元运算符:需要为函数传递 1 个参数。
2)二元运算符:需要为函数传递 2 个参数。
定义为类的成员函数:
函数的参数个数就是运算符的操作个数减1,运算符的操作数有一个成为函数作用的对象,其余的成为成员函数的实参。
1)一元运算符:需要为函数传递 0 个参数。
2)二元运算符:需要为函数传递 1 个参数。

2)可重载的运算符

双目算术运算符+(加),-(减),*(乘),/(除),%(取模)
关系运算符==(等于),!=(不等于),<(小于),>(大于),<=(小于等于),>=(大于等于)
逻辑运算符|| (逻辑或),&&(逻辑与),!(逻辑非)
单目运算符+(正),-(负),*(指针),&(取地址)
自增自减运算符++(自增),–(自减)
位运算符|(按位或),&(按位与),~(按位取反),^(按位异或),<<(左移),>>(右移)
赋值运算符=(赋值),+=(加法赋值), -=(减法赋值),*=(乘法赋值),=(除法赋值),%=(取模赋值),&=(按位与赋值),|=(按位或赋值),^=(按位异或赋值),<<=(左移赋值),>>=(右移赋值)
空间申请与释放new(创建对象),delete(释放对象),new[](创建数组),delete[](释放数组)
其他运算符()(函数调用),->(成员访问),,(逗号),[](下标)

3)不可重载的运算符和符号

成员访问运算符.
成员指针访问运算符.,->
域运算符::
长度运算符sizeof
条件运算符?:
预处理符号#

注:赋值运算符 =【对象成员变量的复制】 和地址运算符 &【返回任何类对象的地址】 ,系统默认提供了重载版本。

4)重载运算符为类的成员函数

代码例子:

#include <iostream>
using namespace std;
class operatorComplex //复数类
{
private:
    double real, imag;

public:
    // 默认构造函数
    operatorComplex()
    {
        real = 0;
        imag = 0;
    }
    //构造函数
    operatorComplex(double r, double i)
    {
        real = r;
        imag = i;
    };
    operatorComplex operator-(const operatorComplex &c);                                    //成员函数
    friend operatorComplex operator+(const operatorComplex &c1, const operatorComplex &c2); //友元函数
    void outCom()
    {
        cout << "(" << real << "," << imag << ")";
    }
};
operatorComplex operatorComplex::operator-(const operatorComplex &c)
{
    operatorComplex temp1; // 声明一个局部对象temp1
    temp1.real = real - c.real;
    temp1.imag = imag - c.imag;
    return temp1; // 返回一个临时对象
}
operatorComplex operator+(const operatorComplex &c1, const operatorComplex &c2)
{
    operatorComplex temp2;
    temp2.real = c1.real + c2.real;
    temp2.imag = c1.imag + c2.imag;
    return temp2; // 返回一个临时对象
}

int main()
{
    operatorComplex c1(1, 2), c2(3, 4), res; // res为默认构造函数
    res.outCom();   // 原来的值 real:0 ,imag:0
    cout << "operator-";
    c2.outCom();   // c2对象参数
    res = res - c2; //(0-3,0-4)
    res.outCom();  //相减后
    cout << endl;  
    c1.outCom();   // c1对象参数 real:1 ,imag:2
    cout << "operator+";
    c2.outCom();   // c1对象参数 real:3 ,imag:4
    res = operator+(c1, c2); //(1+3,2+4)
    res.outCom();
    cout << endl;
    return 0;
};

程序输出结果:

(0,0)operator-(3,4)(-3,-4)
(1,2)operator+(3,4)(4,6)

5)重载运算符为友元函数

代码例子:

#include <iostream>
using namespace std;
class operatorComplex //复数类
{
private:
    double real, imag;

public:
    // 默认构造函数
    operatorComplex()
    {
        real = 0;
        imag = 0;
    }
    //构造函数
    operatorComplex(double r, double i)
    {
        real = r;
        imag = i;
    };
    operatorComplex operator-(const operatorComplex &c); //成员函数
    //友元函数
    friend operatorComplex operator-(const operatorComplex &c1, const operatorComplex &c2);
    // r自己定义数字  res = c1-5
    friend operatorComplex operator-(const operatorComplex &c1, double r)
    {
        return operatorComplex(c1.real - r, c1.imag - r);
    };
    // res = 5-c1
    friend operatorComplex operator-(double r, const operatorComplex &c1)
    {
        return operatorComplex(r - c1.real, r - c1.imag);
    };
    friend operatorComplex operator+(const operatorComplex &c1, const operatorComplex &c2); //友元函数
    void outCom()
    {
        cout << "(" << real << "," << imag << ")";
    }
};
operatorComplex operatorComplex::operator-(const operatorComplex &c)
{
    operatorComplex temp1; // 声明一个局部对象temp1
    temp1.real = real - c.real;
    temp1.imag = imag - c.imag;
    return temp1; // 返回一个临时对象
}
operatorComplex operator-(const operatorComplex &c1, const operatorComplex &c2)
{
    return operatorComplex(c1.real - c2.real, c1.imag - c2.imag); // 返回一个临时对象
};
operatorComplex operator+(const operatorComplex &c1, const operatorComplex &c2)
{
    operatorComplex temp2;
    temp2.real = c1.real + c2.real;
    temp2.imag = c1.imag + c2.imag;
    return temp2; // 返回一个临时对象
}

int main()
{
    operatorComplex c1(1, 2), c2(3, 4), res; // res为默认构造函数
    res.outCom();                            // 原来的值 real:0 ,imag:0
    cout << "operator-";
    c2.outCom();    // c2对象参数
    res = res - c2; //(0-3,0-4)
    // res = 1 - c2; // 普通使用错误的 ,使用友元,需将两个操作都列出来,就不会有问题
    res.outCom(); //相减后
    cout << endl;
    c1.outCom(); // c1对象参数 real:1 ,imag:2
    cout << "operator+";
    c2.outCom();             // c1对象参数 real:3 ,imag:4
    res = operator+(c1, c2); //(1+3,2+4)
    res.outCom();
    cout << endl;
    c1.outCom(); // c1对象参数 real:1 ,imag:2
    cout << "operator-";
    res = c1 - 5; //  (1-5,2-5)
    res.outCom(); //  real:-4 , imag:-3
    res = 5 - c1; //  (5-1,5-2)
    res.outCom(); // real:4 , imag:3
    cout << endl;
    system("pause");
    return 0;
};

程序输出结果:

(0,0)operator-(3,4)(-3,-4)
(1,2)operator+(3,4)(4,6)
(1,2)operator-(-4,-3)(4,3)

6)重载运算符的规则

1)重载后运算符的含义应该符合原有的用法习惯。例如,重载“+”运算符,完成的功能就应该类似于做加法,在重载的“+”运算符中做减法是不合适的。
2)运算符重载不能改变运算符原有的语义,包括运算符的优先级和结合性。
3)运算符重载不能改变运算符操作数的个数及语法结构。
4)不能创建新的运算符,即重载运算符不能超出C++语言允许重载的运算符范围。
5)重载运算符“()”“[]”“->”或者赋值运算符“=”时,只能将它们重载为成员函数,不能重载为全局函数。
6)运算符重载不能改变该运算符用于基本数据类型对象的含义。

2、重载赋值运算符 【=】

1)浅拷贝

同类对象之间可以通过赋值运算符“=”互相赋值。如果没有经过重载,“=”的作用就是将赋值号右侧对象的值一一赋值给左侧的对象。如果赋值的对象中涉及指针或是引里,则它们之间是互相关联的,一个值变化了,另一个值也跟着变化。因为对象中的指针指向的是同一个内存地址。【指针地址是没有改变的,只是改变了值】

注:浅拷贝可能会带来程序上的错误,当对象pl(被复制)消亡时,需要释放构造函数中动态申请的空间,也就是拷贝的整型值占据的空间。而当对象p2(复制了p1)消亡时,也会试图释放这个空间。重复释放同一块空间会产生错误。

2)深拷贝

重载赋值运算符后,赋值语句的功能是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。【通过申请新的空间保存赋值的值】

申请新的空间
pointer &operator=(const pointer &c) //成员函数
{
    if (this = &c)
        return *this;        //避免a=a这样的赋值
    delete this->p;          //释放原来的空间
    this->p = new int(*c.p); //申请新空间保存值
    return *this;
}

新申请了一个保存整数值的空间,两个对象中的指针值不再是相同的,即它们分别指向各自的单元,仅有单元中的值是相同的。析构函数中,因为各对象中指针指向的位置都是不相同的,所以释放时也不会出现问题。
释放掉指针原来指向的空间,再重新申请新的空间,这样可以保证不会与参数对象中指针指向的空间相同。

3、重载流插入运算符和流提取运算符

类库提供的头文件中已经对“<<”和“>>”进行了重载,使之分别作为流插入运算符和流提取运算符,能用来输出和输入C++基本数据类型的数据。cout是 ostream类的对象,cin 是 istream 类的对象,它们都是在头文件iostream 中声明的。因此,凡是用“cout<<”和“cin>>”对基本数据类型数据进行输出/输入的,都要用#include 指令把头文件iostream包含到本程序文件中。

注:流是标准类库,用户程序中只能继承不能修改,所以重载函数不能是流类中的成员,而必须重载为类的友元。

1)重载流插入

插入运算符函数不改变对象的值,可以使用引用 ”类名“ & ”对象名“ 和 ”类名 对象名“。可以重载为成员函数形式和友元形式。

流插入运算符:

左移运算符“<<”可以和cout 一起用于输出

格式
ostream &operator<<(ostream & output,类名&对象){
    ...
    return output;  // output 是类ostream对象的引用 可以定义为任何值 
}

注:output 是类ostream对象的引用 可以定义为任何值【如os】 ,它是cout的别名。即ostream & output = cout

2)重载流提取

提取运算符函数需要返回新的对象值,需要使用引用,即”类名“ & ”对象名“,不能使用”类名 对象名“。重载为友元形式。

流提取运算符:

右移运算符“>>”和cin 一起用于输入

格式:
istream &operator<<(istream & intput,类名&对象){
    ...
    return intput;  // intput 是类istream对象的引用 可以定义为任何值 
}

注:intput是类istream对象的引用 可以定义为任何值【如is】 ,它是cin的别名。即ostream & output = cout

3)代码

#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class operatorComplex //复数类
{
private:
    double real, imag;

public:
    operatorComplex() : real(0), imag(0){};                   // 构造 定义初始值
    operatorComplex(double r, double i) : real(r), imag(i){}; // 构造 变量赋值
    // 友元  插入
    friend ostream &operator<<(ostream &os, const operatorComplex &c)
    {
        if (c.imag >= 0)
            os << c.real << "+" << c.imag << "i"; //将内容 插入
        else
            os << c.real << "-" << (-c.imag) << "i";
        return os;
    }
    // 成员  插入
    ostream &operator<<(ostream &os)
    {
        if (this->imag >= 0)
            os << this->real << "+" << this->imag << "i";
        else
            os << this->real << "-" << (-this->imag) << "i";
    }
    // 友元  提取
    friend istream &operator>>(istream &is, operatorComplex &c)
    {
        string s;
        is >> s;                  // 字符串读入
        int pos = s.find("+", 0); //查找输入字符串中是否含有“+” (-1 没有)
        if (pos == -1)
            pos = s.find("-", 1); //没有“+”,则该值为负数
        string sReal = s.substr(0, pos);
        string sReal2 = s.substr(pos, s.length() - pos - 1);
        // 将输入的值进行更换 输出
        c.imag = atof(sReal.c_str()); // atof 将字符串转换为浮点数
        c.real = atof(sReal2.c_str());
        return is;
    };
};

int main()
{
    operatorComplex c, c1;
    int n;
    cout << "请输入一个复数【a ± bi】 " << endl;
    cin >> c;
    // cin >> c >> c1 >> n;  //输入两个复数 和 一个常量
    c << cout;
    // c1 << (c << cout << ",") << "," << n; // 输出两个更换位置的复数和一个常量
    return 0;
};

输出结果

请输入一个复数【a ± bi】 空格隔开
1+3i
3+1i请按任意键继续. . .

4、重载强制类型转换运算符

在C++中,类型的名字(包括类的名字)本身也是一种运算符,即强制类型转换运算符。
强制类型转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。
经过适当重载后,“(类型名)对象”这个对对象进行强制类型转换的表达式就等价于“对象.operator 类型名()”,即变成对运算符函数的调用。

5、重载自增、自减运算符

自增运算符“++”和自减运算符“–"都可以被重载,但是他们有前置和后置之分。
C++规定,在重载“++”或“–”时,允许写一个增加了无用 int 类型形参的版本,编译器处理“++”或“–”前置的表达式时,调用参数个数正常的重载函数;处理“++”或“–”后置的表达式时,调用多出一个参数【编译器自动以 0 作为实参】的重载函数。

例子:

1、 类 A 的对象Aobject,成员函数重载前置自增运算符
++Aobject   被编译解释  Aobject.operator++();

2、类 A 的对象Aobject,友元函数重载后置自减运算符
Aobject--   被编译解释  operator--(Aobject,0);

3(obj++) 相当于调用了 obj.operator++(0);

4(++obj) 相当于调用了 obj.operator++();

作用域运算符”::“

功能

1.存在具有相同名称的局部变量时,访问全局变量
2.在类之外定义类相关函数。
3.访问类的静态变量
4.在多重继承的情况下,如果两个基类中存在相同的变量名,可以使用作用域运算符来进行区分。
5.限定成员函数所属的类。
6.指出作用域的范围

五、类的继承与派生

1)派生

派生:通过已有的类建立新的类

类的派生

从已有的父类产生一个新的子类,称为类的派生。
派生类继承了基类的所有数据成员和成员函数,具有基类的特性,派生类还可以对成员作必要的增加或调整,定义自己的新特性。
一个基类可以派生出多个派生类,每一个派生类又可以作为基类再派生出新的派生类,因此基类和派生类是相对而言的。
派生类分为单级派生和多级派生
创建派生类对象时,程序首先创建基类对象。

Tip:基类:被定义为包含所有实体共性的class类型,父类(相对于派生类)

定义

class 派生类名 : 继承方式说明符 基类名 { 
    类体
	成员列表
};

eg:
class BaseClass{  //基类 【也可以为空基类class BaseClass{}】
    int v1,v2;
}
class DerivedClass : public BaseClass{  //派生类
    int v3;
}

代码例子

#include <iostream>
#include <string>
using namespace std;
class BaseClass //声明基类
{
public:
    void get_1() //输入姓名及性别
    {
        cin >> name >> sex;
    }
    void coutValue_1() //输出姓名性别
    {
        cout << "姓名:" << name << " "
             << "性别:" << sex << endl;
    }

private:
    string sex;
    string name;
};
class DerivedClass : public BaseClass //声明派生类,继承方式为public  使用私有继承,基类的公有方法将成为派生类的私有方法
{
public:
    void get_2() //输入地址以及姓名性别
    {
        get_1(); //在派生类中调用基类的get_1函数
        cin >> address;
    }
    void coutValue_2() //输出地址以及姓名性别
    {
        coutValue_1(); //在派生类中调用基类的coutValue_1函数
        cout << "地址:" << address << endl;
    }

private:
    string address;
};
int main()
{
    DerivedClass s; //定义派生类对象
    s.get_2();  // 调用派生类的成员,相继也调用了基类的get_1
    s.coutValue_2();  
    return 0;
}

程序结果:

泰语小分队 女
广州
姓名:泰语小分队 性别:女
地址:广州
请按任意键继续. . .

类的大小

派生类对象占用的存储空间大小:等于基类成员变量占用的存储空间大小加派生类对象自身成员变量占用的存储空间大小。可用sizeof() 函数计算对象所占用的字节数。

Tip:对象的大小与普通成员变量有关,与成员函数和类中的静态成员变量无关,即普通成员函数、静态成员函数静态成员变量、静态常量成员变量等均对对象的大小没有影响

int 型变量占 4 个字节
char 型变量占 1 个字节
在64位系统中 指针 占 8 个字节
在32位系统中 指针 占 4 个字节

继承关系的特殊性

如果基类是某类的友元,则这种友元关系是被继承的。基类的友元不一定是派生类的友元,基类的成员函数是某类的友元函数,则其作为派生类继承的成员函数仍是某类的友元函数。
若派生类重写了基类的友元函数,还想要访问声明了该友元函数的类的私有成员,还需把派生类重写的函数定义为该类的友元函数。

代码例子:

#include <iostream>
#include <string>
using namespace std;
class another;
class BaseClass //声明基类
{
private:
    float x;

public:
    void print(const another &K);
};

class DerivedClass : public BaseClass //声明派生类,继承方式为public  使用私有继承
{
private:
    float y;

public:
    void print(const another &K);  // 重写基类的成员函数
};
class another
{
private:
    int aa;

public:
    another()
    {
        aa = 100;
    };                                              //构造 初始化
    friend void BaseClass::print(const another &K); //基类的成员函数声明为本类的友元
    friend void DerivedClass::print(const another &K); //派生类的重写基类成员函数声明为本类的友元
};
void BaseClass::print(const another &K)
{
    cout << "Base:" << K.aa << endl;
}
void DerivedClass::print(const another &K)
{
    // 需要在another 类声明友元函数,否则报错
    cout << "Derived:" << K.aa << endl;
}
int main()
{
    BaseClass b;
    DerivedClass d;
    another an; // aa 初始化
    b.print(an);  
    d.print(an);  // 若不重写print 函数,输出的是基类的 print函数的内容
    return 0;
}

程序结果:

Base:100
Derived:100
请按任意键继续. . .

Tip:C++的继承性允许派生类继承基类的部分成员(除了基类的构造函数和析构函数不能继承),并允许增加新的成员或重定义基类的成员。

有继承关系的类之间的访问

派生类和基类都可以定义自己的成员变量和成员函数,派生类中的成员函数可以访问基类中公有成员变量,但不能直接访问基类中的私有成员变量【不能在派生类使用 “ 基类对象名.基类私有成员函数(参数) ” 或者 “ 基类对象名.基类私有成员变量 ” 或者 “ 基类名 :: 基类私有成员”】。
在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用域范围不同,是相互包含的两个层,派生类在内层,基类在外层。

注:如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用基类名和作用域分辨符来限定

// 使用作用域符号 ::
派生类对象名.基类名::基类成员;

#eg:
c.className::function();

Tip:若要在派生类中访问基类的私有成员,可通过基类的公有成员函数访问。

class B:public A{
private:
    int y;
public:
    B(int x1, int y1) : A(x1) {
        y = y1;
    }
    A::function();               //访问基类A的函数function
};
int main(){
   B b; //派生类 B 的对象b
   b.A::function();  //访问基类A的函数function
}

访问范围说明符(public、private、protected)

派生类访问基类的成员:
public(公有):成员公开,可直接访问
private(私有):成员隐藏,可通过公有成员函数访问
protected(保护):成员隐藏,可直接访问

Tip:保护成员的访问范围比私有成员访问范围大,能访问私有成员的地方都能访问保护成员。基类中的保护成员可以在派生类的成员函数中被访问【该作用下的基类,其他基类不可访问】。
使用保护成员:起到了隐藏作用,也避免了派生类成员要访问它们时只能间接访问所带来的麻烦。

代码示例:

#include <iostream>
#include <string>
using namespace std;
class BaseClass //声明基类
{
private:
    string name;

protected:
    string address;

public:
    string sex = "女";
    void setName(string n)
    {
        // this->name = n;
        name = n;
        return;
    }
    void printName()
    {
        cout << "姓名:" << name << endl;
    };
};

class DerivedClass : public BaseClass //声明派生类,继承方式为public
{
public:
    void setValue(string adr)
    {
        address = adr;
    }
    void printValue()
    {
        cout << "基类的保护成员address:" << address << endl;
    }
    void print()
    {
        BaseClass::setName("小蓉蓉"); //获取基类的私有成员name ,调用基类 setName函数
        BaseClass::printName();
    }
};
int main()
{
    DerivedClass d;          // 派生类对象
    BaseClass b;             //基类对象
    b.setName("泰语小分队"); // 基类对象直接获取成员函数
    b.printName();
    d.print();
    string sex = d.BaseClass::sex; //通过派生类获取基类成员函数
    cout << "性别:" << sex << endl;
    d.setValue("广州"); // 派生类获取基类保护成员 address
    d.printValue();
    return 0;
}

程序输出:

姓名:泰语小分队
姓名:小蓉蓉
性别:女
基类的保护成员address:广州
请按任意键继续. . .

多重派生

多重派生:一个派生类可以同时有多个基类【多个类派生一个类】,多个基类的所有成员除了构造函数和析构函数外都被派生继承。
单继承/单重继承:一个基类派生一个派生类。

格式:
class 派生类名: 继承方式说明符 基类名 1,继承方式说明符 基类名2,...{
   类体
}

Tip:多重继承情况下如果多个基类间成员重名时,会产生二义性。
1)避免二义性:
使用基类名和作用域分辨符 “::” 来标识成员【基类名::同名变量名;】
2)派生类新增与基类相同的成员名,可使用【派生类对象名.成员名】 或 【派生类对象指针 -> 成员名】可以唯一标识和访问派生类新增成员。
3)对于派生类而言,不加类名限定时默认的是派生类的成员,要访问基类重名成员,通过类名加以限定。

代码示例:

#include <iostream>
#include <string>
using namespace std;
class BaseClassX //声明基类
{
public:
    int x;
    BaseClassX();
    BaseClassX(int x1) : x(x1)
    {
        cout << "基类 BaseClassX" << endl;
    }; //带参构造函数 初始化
    // ~BaseClassX(); //析构函数
};
BaseClassX::~BaseClassX()
{
    cout << "基类 BaseClassX 析构函数" << endl;
}
class BaseClassY //基类
{
public:
    int y, x; // x 与基类BaseClassX 的 x 重名
    BaseClassY();
    BaseClassY(int y1, int x2) : y(y1), x(x2)
    {
        cout << "基类 BaseClassY" << endl;
    };
    // ~BaseClassY();
};

class DerivedClass : public BaseClassX, public BaseClassY //声明派生类,继承基类 BaseClassX,BaseClassY
{
public:
    int y; //与基类BaseClassY 的成员变量y 重名 【以派生的变量为准】
    DerivedClass();
    DerivedClass(int y2, int x3, int y1) : y(y2), BaseClassX(x3), BaseClassY(x3, y1) // 基类BaseClassY的x值与基类BaseClassX 的x值不一样
    {
        cout << "派生类 DerivedClass" << endl;
    };
    // ~DerivedClass();
};
int main()
{
    DerivedClass d(100, 200, 300); // 派生类对象
    cout << "DerivedClass y:" << d.y << endl;
    cout << "BaseClassX 的x:" << d.BaseClassX::x << endl;
    cout << "BaseClassY 的y:" << d.BaseClassY::y << endl;
    cout << "BaseClassY 的x:" << d.BaseClassY::x << endl;
    return 0;
}

程序结果:

基类 BaseClassX
基类 BaseClassY
派生类 DerivedClass
DerivedClass y:100
BaseClassX 的x:200
BaseClassY 的y:200
BaseClassY 的x:300
请按任意键继续. . .

2)访问控制

设计继承类时,需要使用继承方式说明符指明派生类的继承方式。继承方式说明符可以是public (公有继承)、private (私有继承)或 protected (保护继承)

基类的公有成员在派生类中仍然是公有的这个要取决于派生类的继承方式,如果派生类是公有继承则基类的公有成员在派生类中仍然是公有的,如果派生类是私有继承则基类所有成员都在派生类变成私有的。

公有继承(public)

当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变【派生类的成员函数可以直接访问它们】,而基类的私有成员在基类外不可直接访问【须通过基类的公有成员函数访问基类的私有成员】。

class Base{...}; // 基类
class Derived : public Base{...} // 派生类

公有继承的访问控制
各成员**派生类中 **基类与派生类外
基类的公有成员直接访问直接访问
基类的保护成员直接访问调用公有函数访问
基类的私有成员调用公有函数访问调用公有函数访问
从基类继承的公有成员直接访问直接访问
从基类继承的保护成员直接访问调用公有函数访问
从基类继承的私有成员调用公有函数访问调用公有函数访问
派生类中定义的公有成员直接访问直接访问
派生类中定义的保护成员直接访问调用公有函数访问
派生类中定义的私有成员直接访问调用公有函数访问

类型兼容规则

类型兼容规则:在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,也称为赋值兼容规则。

在公有派生的情况下,有3 条类型兼容规则
(1)派生类的对象可以赋值给基类对象。
(2)派生类对象可以用来初始化基类引用
(3)派生类对象的地址可以赋值给基类指针,即派生类的指针可以赋值给基类的指针

代码例子
#include <iostream>
#include <string>
using namespace std;
class BaseClass //声明基类
{
    int baseValue; //默认私有成员
public:
    BaseClass();      //构造
    BaseClass(int bv) //初始化
    {
        baseValue = bv;
    };
    void print()
    {
        cout << "BaseClass基类:" << baseValue << endl;
    }
    void print(int a)
    {
        cout << "间隔baseValue:" << baseValue << endl;
    }
};

class DerivedClass : public BaseClass //声明派生类,公有继承基类 BaseClass
{
    int derivedValue;

public:
    DerivedClass(int dv) : BaseClass(2 * dv) //构造 初始化【基类对象作为参数】
    {
        derivedValue = dv; //  形参BaseClass(2 * dv)
    }
    void print()
    {
        cout << "DerivedClass派生类:" << derivedValue << endl;
        // BaseClass::print(1);
    }
};
int main()
{
    BaseClass b(10);    // 基类对象
    DerivedClass d(20); // 派生类对象
    b.print();
    d.print();
    b = d; //派生类对象赋值给基类对象
    cout << "派生类对象赋值给基类对象后" << endl;
    b.print();
    d.print();
    return 0;
}

程序结果:

BaseClass基类:10
DerivedClass派生类:20
派生类对象赋值给基类对象后
BaseClass基类:40
DerivedClass派生类:20
请按任意键继续. . .

派生类对象赋值给基类对象 (如程序中的),在赋值号“=”没有被重载的情况下,所做的作就是将派生类对象中的基类对象逐个字节地复制到“=”左边的基类对象中。

类型兼容规则还允许下列赋值语句:
BaseClass&r=d; //派生类对象初始化基类引用
BaseClasspb=&d; //派生类对象地址赋值给基类指针
DerivedClass
pd=&d;
pb=pd; // 派生类指针赋值给基类指针

Tip:在公有派生的情况下,可以说,派生类对象也是基类对象,任何本该出现基类对象的地方,如果出现的是派生类的对象,也是没有问题的。但如果派生方式不是 public,而是private或protected,那么上面这个结论就不成立了。

私有继承(private)

当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可以直接访问【无论是通过派生类的成员函数还是通过派生类的对象,都无法直接访问从基类继承的私有成员】

Tip:经过私有继承之后,所有基类的成员都成为派生类的私有成员或不可直接访问的成员如果进一步派生,基类的全部成员就无法在新的派生类中被访问。

class Base{...}; // 基类
class Derived : private Base{...}//第一级派生类
class subDerived : public Derived{...}//第二级派生类

私有继承的访问控制
第一级派生类中第二级派生类中基类与派生类外
基类的公有成员直接访问不可访问不可访问
基类的保护成员直接访问不可访问不可访问
基类的私有成员调用公有函数访问不可访问不可访问

Tip:定义派生类时,继承方式可以省略,继承方式是私有继承。

保护继承(protected)

基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接访问。

派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但在类外通过派生类的对象无法直接访问它们。

class Base{...}; // 基类
class Derived : protected Base{...}//派生类

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

在执行一个派生类的构造函数之前,总是先执行基类的构造函数。派生类对象消亡时,先执行派生类的析构函数,在执行基类的构造函数。

Tip:如果对基类对象初始化时需要调用基类的带有形参表的构造函数,则派生类就必须声明构造函数

定义派生类构造函数的格式

派生类名::派生类名(参数表):基类名1(基类1初始化参数表),...,基类名m(基类m初始化参数表),成员对象 名1(成员对象1初始化参数表),成员对象名n(成员对像n初始化参数表)
{
   派生类构造函数函数体 //其他初始化操作
}

派生类构造函数执行顺序

1)调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
2)对派生类新增的成员变量初始化,调用顺序按照它在类中声明的顺序
3)执行派生类的构造函数体的内容

派生类析构函数执行顺序

1)派生类析构函数
2)基类析构函数(从右向左)

// 基类A,B 共同派生子类 C 
class C:public A,public B{...}
1)构造函数执行的顺序
A、B、C
2)析构函数执行的顺序
C、B、A

代码例子

#include <iostream>
#include <string>
using namespace std;
class BaseClass1 //声明基类
{
private:
    int baseX; //成员变量

public:
    BaseClass1() {}        //无参构造
    BaseClass1(int bx = 0) // 带参构造
    {
        baseX = bx;
        cout << "基类BaseClass1:" << baseX << endl;
    }
    ~BaseClass1()
    {
        cout << "基类BaseClass1析构" << endl;
    }
    void print() //成员函数
    {
        cout << "基类BaseClass1成员函数:" << baseX << endl;
    }
};

class BaseClass2 //声明基类
{
private:
    int baseY; //成员变量

public:
    BaseClass2() {}        //无参构造
    BaseClass2(int by = 0) // 带参构造
    {
        baseY = by;
        cout << "基类BaseClass2:" << baseY << endl;
    }
    ~BaseClass2()
    {
        cout << "基类BaseClass2析构" << endl;
    }
    void print() //成员函数
    {
        cout << "基类BaseClass2成员函数:" << baseY << endl;
    }
};

class DerivedClass : public BaseClass1, public BaseClass2 //声明派生类,公有继承基类 BaseClass1和BaseClass2
{
private:
    int derivedZ;

public:
    DerivedClass(int x, int y, int z) : BaseClass1(x), BaseClass2(y) //基类初始化
    {
        derivedZ = z;
        cout << "派生类DerivedClass:" << derivedZ << endl;
    }
    ~DerivedClass()
    {
        cout << "派生类DerivedClass析构" << endl;
    }
    void print()
    {
        // 执行基类的成员函数
        BaseClass1::print();
        BaseClass2::print();
        cout << "DerivedClass派生类:" << derivedZ << endl;
    }
};
int main()
{
    DerivedClass d(10, 20, 30);
    d.print();
    return 0;
}

程序结果

基类BaseClass1:10
基类BaseClass2:20
派生类DerivedClass:30
基类BaseClass1成员函数:10
基类BaseClass2成员函数:20
DerivedClass派生类:30
派生类DerivedClass析构
基类BaseClass2析构
基类BaseClass1析构

派生类的复制构造函数

如果要为派生类编写复制构造函数,一般也要为基类相应的复制构造函数传递参数,但并不是必须的。
例子:

// 基类
class A{
 public:
    A(const A &s){  // 复制构造函数 【s为形参】
        函数体
    }
}

//派生类
class B{
  public:
    //1)派生类B的复制构造函数中调用了基类A的复制构造函数
    B(const B &v):A::A(v),f(v.f){  // A::A(v)【基类复制构造函数传值】  v.f【v对象的f成员】
       函数体
    }
    //2)派生类B的复制构造函数中,没有调用了基类A的复制构造函数
    B(const B &v):f(v.f){  //  v.f【v对象的f成员】
       函数体
    }
}

Tip:对于一个类,如果程序没有定义复制构造函数,则编译器会自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,对派生类新增的成员对象一一执行复制。

4)类之间的关系

类和类之间的两种基本关系:继承和组合

继承

继承关系也称为“ is a ”关系或“是”关系。派生类对象也是基类的对象。

“ is a ” 关系具有传递性

组合

继承关系也称为“ has a ”关系或“有”关系。表现为封闭类【一个类以另一个类的对象作为成员变量】

“ has a ” 关系表示的是类的包含关系

封闭类的派生

封闭类:一个类的成员变量是另一个类的对象

格式
// 内嵌对象1(形参表),内嵌对象2(形参表)...  是初始化列表,其作用是对内嵌对象进行初始化
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表)...{
   类体
}

在派生类也是封闭类的情况下,构造函数的初始化列表不但要指明基类对象的初始化方式,还要指明成员对象的初始化方式。

1)生成派生类对象时,构造函数的调用顺序是:先根据派生层次从上至下依次执行所有基类的构造函数,最后执行自身的构造数。如果某个类是封闭类,则在执行本类构造函数之前,先按照成员对象的定义顺序执行名个成员对象所属类的构造函数。
2)派生类对象消亡时,执行析构函数的次序与执行构造函数的次序相反。 派生类构造函数中的某些初始化可能是基于基类的,所以规定构造函数从类层次的最上层处开始,一层层地执行构造函数。

代码实例
#include <iostream>
#include <string>
using namespace std;

class myDate
{
public:
    myDate();
    myDate(int);
    myDate(int, int);
    myDate(int, int, int);
    myDate(const myDate &d);
    ~myDate(); // 析构函数
    void setDate(int, int, int);
    void setDate(myDate);
    myDate getDate();
    void setYear(int);
    int getMonth();
    void printDate() const;

private:
    int year, month, day;
};
// 无参构造函数
myDate::myDate()
{
    year = 1970;
    month = 1;
    day = 1;
    cout << "myDate默认构造函数" << endl;
}

myDate::myDate(int y)
{
    year = y;
    month = 1;
    day = 1;
    cout << "myDate带1个构造函数" << endl;
}

myDate::myDate(int y, int m)
{
    year = y;
    month = m;
    day = 1;
    cout << "myDate带2个构造函数" << endl;
}

myDate::myDate(int y, int m, int d)
{
    year = y;
    month = m;
    day = d;
    cout << "myDate带3个构造函数" << endl;
}
// 复制构造函数
myDate::myDate(const myDate &d)
{
    year = d.year;
    month = d.month;
    day = d.day;
    cout << "myDate_COPY构造函数" << endl;
}

myDate::~myDate()
{
    cout << "myDate析构函数" << endl;
}
// 基类成员函数
void myDate::setDate(int y, int m, int d)
{
    year = y;
    month = m;
    day = d;
    return;
}
// 成员函数(参数是对象) 【封闭类】
void myDate::setDate(myDate oneD)
{
    year = oneD.year;
    month = oneD.month;
    day = oneD.day;
    return;
}

myDate myDate::getDate()
{
    myDate d;
    d.year = year;
    d.month = month;
    d.day = day;
    return d;
}

void myDate::setYear(int y)
{
    year = y;
    return;
}

int myDate::getMonth()
{
    return month;
}

void myDate::printDate() const
{
    cout << year << "/" << month << "/" << day;
    return;
}
// 基类
class CStudent
{
public:
    CStudent();
    CStudent(string, myDate);
    ~CStudent();
    void setStudent(string, myDate);
    void setName(string);
    string getName();
    void setBirthday(myDate);
    myDate getBirthday();
    void printInfo() const;

private:
    string name;
    myDate birthday;
};

CStudent::CStudent() : name("Noname"), birthday(myDate())
{
    cout << "CStudent默认构造函数" << endl;
}

CStudent::CStudent(string str, myDate d) : name(str), birthday(d)
{
    cout << "CStudent有参构造函数" << endl;
}
CStudent::~CStudent()
{
    cout << "CStudent析构函数" << endl;
}

void CStudent::setStudent(string s, myDate d)
{
    name = s;
    birthday.setDate(d);
    return;
}

void CStudent::setName(string n)
{
    name = n;
    return;
}

string CStudent::getName()
{
    return name;
}

void CStudent::setBirthday(myDate d)
{
    birthday.setDate(d);
    return;
}

myDate CStudent::getBirthday()
{
    return birthday;
}

void CStudent::printInfo() const
{
    cout << "姓名:" << name << "\t生日:";
    birthday.printDate(); // 调用类myData的成员函数
    cout << endl;
}
// 派生类【公有继承CStudent】
class CUndergraduateStudent : public CStudent
{
private:
    string department;

public:
    CUndergraduateStudent();
    CUndergraduateStudent(string, myDate);
    ~CUndergraduateStudent();
    void setDep(string);
    void printInfo() const;
};

CUndergraduateStudent::CUndergraduateStudent()
{
    cout << "CUndergraduateStudent默认构造函数" << endl;
}

CUndergraduateStudent::CUndergraduateStudent(string str, myDate d) : CStudent(str, d)
{
    cout << "CUndergraduateStudent有参构造函数" << endl;
}
CUndergraduateStudent::~CUndergraduateStudent()
{
    cout << "CUndergraduateStudent析构函数" << endl;
}

void CUndergraduateStudent::setDep(string dep)
{
    department = dep;
}

void CUndergraduateStudent::printInfo() const
{
    CStudent::printInfo();
    cout << "院系:\t" << department << endl
         << endl;
}

int main()
{
    CUndergraduateStudent s2("小张", myDate());
    return 0;
}

程序结果

myDate默认构造函数
myDate_COPY构造函数
myDate_COPY构造函数
CStudent有参构造函数
myDate析构函数
CUndergraduateStudent有参构造函数
myDate析构函数
请按任意键继续. . .

互包含关系的类

循环依赖:两个类相互引用。

Tip:在使用一个类之前,必须先定义该类【使用前向引用声明】
前向引用声明:在引用未定义类之前,将该类的名字告诉编译器,使编译器知道那是一个类。当程序中使用这个类名时,编译器就不会认为是个错误,而类的完整定义可以放在程序的其他地方。

eg:
class B;  //前向引用说明
class A{
   public:
      void f(B b);  //以类B对象为形参的成员函数
};
class B{
   public:
      void g(A a); //以类A对象为形参的成员函数
}

Tip:在提供一个完整类定义之前,不能定义该类对象,也不能在内联成员函数中使用该类对象。

5)多层次的派生

多层次的派生:类A派生类B,类B可以派生类C,类C又能派生类D。

直接基类与间接基类

派生类的成员包括派生类自己定义的成员、直接基类中定义的成员及所有间接基类中定义的全部成员

1)当生成派生类的对象时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后执行派生类自身的构造函数。
2)当派生类对象消亡时,会先执行自身的析构函数,然后自底向上依次执行各个基类的析构函数。

6)基类与派生类指针的相互转换

在公有派生的情况下,因为派生类对象也是基类对象,所以派生类对象可以赋给基类对象。
对于指针类型,可以使用基类指针指向派生类对象,也可以将派生类的指针直接赋值基类指针。但即使基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类中没有而仅在派生类中定义的成员函数

Tip:当基类对象不是派生不对象时,当派生类指针指向基类对象时,必须要将基类对象进行强制类型转换,才能赋给派生类指针。

代码例子:

#include <iostream>
#include <string>
using namespace std;
class CBase
{
protected:
    int n;

public:
    // 构造初始化
    CBase(int i) : n(i) {}
    void Print()
    {
        cout << "基类CBase:n=" << n << endl;
    }
};
class CDerived : public CBase
{
public:
    int v;
    // CBase(i) 【基类初始化传值】
    CDerived(int i) : CBase(i), v(2 * i) {}
    void Func()
    {
        cout << "派生类成员函数" << endl;
    };
    void Print()
    {
        cout << "派生类CDerived:n=" << n << endl; // 基类成员
        cout << "派生类CDerived:v=" << v << endl;
    }
};

int main()
{
    CDerived objDerived(3);
    CBase objBase(4);
    CBase *pBase = &objDerived; // 基类指针指向派生类对象
    CDerived *pDerived;         // 派生类指针
    pDerived = &objDerived;
    cout << "派生类指针调用函数" << endl;
    pDerived->Print(); // 调用的是派生类中函数
    pBase = pDerived;  // 基类指针 = 派生类指针 【正确,不需要类型转换】
    cout << "基类指针调用函数" << endl;
    pBase->Print();
    pDerived->Func(); // 派生类调用派生类自己定义的成员函数
    // pBase->Func();  //【错误】:基类不能调用除了自己的派生类定义的成员函数
    // pDerived = pBase; //【错误】:派生类指针  不能直接赋值  基类指针(需要类型强制转换)
    pDerived = (CDerived *)pBase; // 强制类型转换
    cout << "强制类型转换后 派生类指针调用函数" << endl;
    pDerived->Print();
    return 0;
}

程序结果:

派生类指针调用函数
派生类CDerived:n=3
派生类CDerived:v=6
基类指针调用函数
基类CBase:n=3
派生类成员函数
强制类型转换后 派生类指针调用函数
派生类CDerived:n=3
派生类CDerived:v=6
请按任意键继续. . .

Tip:基类引用也可以强制转换为派生类引用。
将基类指针强制转换为派生类指针,或将基类引用强制转换为派生类引用,都会有安全隐患。C++提供了(dynamic_cast 强制类型转换运算符,可以判断这两种转换是否安全(即转换后的指针或引用是否真的指向或引用派生类对象)。

六、多态与虚函数

1、多态

多态分为编译时多态和运行时多态。编译时多态主要是指函数的重载(包括运算符的重载)。运行时多态则和继承、虚函数等概念有关。

1)多态

静态多态:在编译阶段就建立函数代码与函数调用【静态联编或静态绑定】。参数的不同而调用不同的同名函数
动态多态:函数调用与代码入口地址绑定需要在运行时刻才能确定【动态联编或动态绑定】。不同对象调用同名函数。

实现动态绑定需要满足的条件(类之间满足赋值兼容)
1)必须声明虚函数
2)通过基类类型的引用或者指针调用虚函数

多态:为了接口的复用。通过基类的指针或引用调用基类或派生类中都有的同名虚函数时,如果基类指针指向的是基类对象,执行的就是基类的虚函数;如果基类指针指向的是派生类对象,执行的就是派生类的虚函数。

Tip:多态性肯定是调用同名的函数。

2)虚函数(virtual)

在函数声明时前面加了 virtual 关键字的成员函数。virtual 关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。静态成员函数不能是虚函数。包含虚函数的类称为“多态类”。

格式
virtual 函数返回值类型 函数名(形参表);

注:
1)虽然将虚函数声明为内联函数不会引起错误,但因为内联函数是在编译阶段进行静态处理的,而对虚函数的调用是动态绑定的,所以虚函数一般不声明为内联函数。
2)派生类重写基类的虚函数实现多态,要求函数名、参数列表及返回值类型要完全相同。
3)基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
4)只有类的非静态成员函数才能定义为虚函数,静态成员函数和友元函数不能定义为虚函数。
5)如果虚函数的定义是在类体外,则只需在声明函数对添加 virtual 关键字,定义时不加virtual关键字。
6)构造函数不能定义为虚函数。最好也不要将 operator=定义为虚函数,因为使用时容易混淆。
7)不要在构造函数和析构函数中调用虚函数。在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。

不可定义为虚函数:

1)全局函数不能声明为虚函数。

虚函数是为了与继承机制配合实现多态的,而全局函数(非成员函数)不属于基个类,没有继承关系,只能被重载,不能被覆盖,声明为虚函数也起不到多态的作用。因此编译器会在编译时绑定全局函数。

2)构造函数不能声明为虚函数。

构造函数一般用来初始化对象,只有在一个对象生成之后,才能发挥多态作用。如果将构造函数声明为虚函数,则表现为在对象还没有生成的时候来定义它的多态,这两点是不统一的。另外,构造函数不能被继承,因而不能声明为虚函数。

3)静态成员函数不能声明为虚函数。

静态成员函数对于每个类来说只有一份代码,所有的对象都共享这份代码,它不归某个对象所有,所以也没有动态绑定的必要性。

4)内联成员函数不能声明为虚函数。

定义内联函数的目的是为了在代码中直接展开减少函数调用的开销。定义虚函数的目的是为了在继承后对象能够准确地执行自己的动作这两个目的南辕北辙,是不可能统一的。内联函数在编译时被展开,虚函数在运行时才能动态地绑定函数。

5)友元函数不能声明为虚函数。

友元函数不属于类的成员函数,不能被继承,没有声明为虚函数的必要。

代码例子:

允许通过基类的指针或引用来访问基类和派生类的同名函数。

#include <iostream>
using namespace std;
class Teacher
{
public:
    virtual void work()
    {
        cout << "上课" << endl;
    }
};

class MathTeacher : public Teacher  // 派生类
{
public:
    void work()
    {
        cout << "数学课" << endl;
    }
};

class ChinsesTeachar : public Teacher
{
public:
    void work()
    {
        cout << "语文课" << endl;
    }
};

class EnglishTeachar : public Teacher
{
public:
    void work()
    {
        cout << "英语课" << endl;
    }
};

int main()
{
    cout << "上课啦!!!" << endl;
    MathTeacher tea; // 创建对象 
    tea.Teacher::work(); // 由于Teacher基类使用了 virtual 虚函数,则数学老师可以获取基类的 work 函数
    tea.MathTeacher::work(); //没有虚函数只能这样调用基类的函数
    //定义一个指针数组
    Teacher **teacher = new Teacher *[3]();
    teacher[0] = new MathTeacher();
    teacher[1] = new ChinsesTeachar();
    teacher[2] = new EnglishTeachar(); // EnglishTeachar类名
    for (int i = 0; i < 3; ++i)
    {
        teacher[i]->work();
    }
    system("pause"); // 解决文件控制台闪退
    return 0;
}

在这里插入图片描述

3)多态的实现原理

多态的关键在于通过指针或引用调用一个虚函数时,编译阶段不能确定到底调用的是基类还是派生类的函数,运行时才确定

代码例子
#include <iostream>
#include <string>
using namespace std;
class A // 基类
{
public:
    int i;
    virtual void func() {}  // 虚函数
    virtual void func1() {} // 虚函数
};
class B : public A // 公有派生
{
    int i;
    void func() {}
};
int main()
{
    // 64 位计算机运行
    cout << "基类A空间大小:" << sizeof(A) << "\n派生类B空间大小:" << sizeof(B) << endl;
    return 0;
}

程序输出:

// 基类 有 virtual (虚函数)
基类A空间大小:16
派生类B空间大小:16
请按任意键继续. . .
    
// 基类 没有 virtual (虚函数)
基类A空间大小:4
派生类B空间大小:8
请按任意键继续. . .

Tip:当类中定义了虚函数时,类对象占用的空间变太了,实际上,这是编译系统为类对象自动添加的部分,是一块连续的内存,其中存储的是虚函数表的地址。
【每一个有虚函数的类都有一个虚函数表,虚函数表编译需生成的,程序运行时被载入内存。一个类的虚函数表中列出了该类的全部虚函数地址。虚函数表是类中所有对象共的,该类的任何对象中都保在指向该虚函数表的指针,这就是导致含虚函数类的对象占用空间增大的原因。】

4)通过基类指针实现多态

代码例子:

#include <iostream>
#include <string>
using namespace std;
class A // 基类
{
public:
    // 虚函数
    virtual void print()
    {
        cout << "A虚函数" << endl;
    }
    // string name;
};
class B : public A // 公有派生
{
    virtual void print()
    {
        cout << "B虚函数" << endl;
    }
};
class C : public A // 公有派生
{
    virtual void print()
    {
        cout << "C虚函数" << endl;
    }
};
class D : public B // 公有派生
{
    virtual void print()
    {
        cout << "D虚函数" << endl;
    }
};
int main()
{
    // 创建对象
    A a;
    B b;
    C c;
    D d;
    A *pa = &a;  // 基类指针pa 指向基类对象a
    B *pb = &b;  // 派生类指针pb指向派生类对象b
    pa->print(); // 多态  目前指向基类对象a
    pa = pb;     // 派生类指针赋给基类指针,pa 指向派生类对象b
    pa->print(); // 多态,  目前指向的是派生类对象b
    pa = &c;     // 基类指针pa 指向派生类对象c
    pa->print(); // 多态,目前指向派生类对象c
    pa = &d;     // 基类指针pa指向派生类对象d
    pa->print(); // 多态, 目前指向派生类对象d
    return 0;
}

程序结果:

A虚函数
B虚函数
C虚函数
D虚函数
请按任意键继续. . .

5)通过基类引用实现多态

代码例子:

#include <iostream>
#include <string>
using namespace std;
class A // 基类
{
public:
    // 虚函数
    virtual void print()
    {
        cout << "A虚函数" << endl;
    }
    // string name;
};
class B : public A // 公有派生
{
    virtual void print()
    {
        cout << "B虚函数" << endl;
    }
};
// 默认传入基类对象
void printInfo(A &r)
{
    // 多态,  使用基类引用调用哪个成员函数(print)取决于r引用了哪个类的对象
    r.print();
}
int main()
{
    // 创建对象
    A a;
    B b;
    printInfo(a); // 使用基类对象,调用基类的函数
    printInfo(b); // 使用派生类对象,调用派生类的函数
    return 0;
}

程序结果:

A虚函数
B虚函数
请按任意键继续. . .

Tip:多态:可以简单理解为同一条函数调用语句能调用不同的函数体,或者说,对不同的对象发送同样的消息,使得不同对象有各自不同的行为。

若成员函数不是虚函数,则都指向基类

#include <iostream>
#include <string>
using namespace std;
class A // 基类
{
public:
    // 成员函数
    void print()
    {
        cout << "A成员函数" << endl;
    }
    // string name;
};
class B : public A // 公有派生
{
    void print()
    {
        cout << "B成员函数" << endl;
    }
};
// 默认传入基类对象
void printInfo(A &r)
{
    //   使用基类引用调用哪个成员函数(print)取决于r引用了哪个类的对象
    r.print();
}
int main()
{
    // 创建对象
    A a;
    B b;
    printInfo(a); // 使用基类对象,调用基类的函数
    printInfo(b); // 使用派生类对象,调用的还是基类的函数
    return 0;
}

程序结果:

A成员函数
A成员函数
请按任意键继续. . .

2、多态与非多态

#include <iostream>
#include <string>
using namespace std;
class A // 基类
{
public:
    void func1()
    {
        cout << "A::func1" << endl;
    }
    virtual void func2() // 虚函数
    {
        cout << "虚函数A::func2" << endl;
    }
};
class B : public A // 公有派生
{
public:
    virtual void func1() // 派生类重新定义虚函数
    {
        cout << "虚函数B::func1" << endl;
    }
    void func2() // 自动成为虚函数
    {
        cout << "虚函数B::func2" << endl;
    }
};
class C : public B // 公有派生
{
public:
    void func1() // 自动成为虚函数
    {
        cout << "虚函数C::func1" << endl;
    }
    void func2() // 自动成为虚函数
    {
        cout << "虚函数C::func2" << endl;
    }
};
int main()
{
    // 创建对象
    C obj;
    A *pa = &obj; // 基类A指针pa指向派生类对象
    B *pb = &obj;
    pa->func2(); // 多态
    pa->func1(); // 不是多态
    pb->func1(); // 多态
    return 0;
}

程序结果:

虚函数C::func2
A::func1
虚函数C::func1
请按任意键继续. . .

Tip:派生类中继承自基类的虚函数,可以写virtual 关键字,也可以省略这个关键字,这不影响派生类中函数也是虚函数。

3、虚析构函数

虚析构函数没有返回值类型,没有参数。

目的:为了在对象消亡时实现多态。【在使用指针或者引用时可以动态绑定,实现运行时多态,保证使用基类类型的指针能够调用适当的析构函数针对不同对象进行清理工作】

格式:

virtual ~类名();

例子:

1)不使用虚析构函数
#include <iostream>
#include <string>
using namespace std;
class Base // 基类
{
public:
    Base()
    {
        cout << "Base构造函数" << endl;
    }
    ~Base()
    {
        cout << "Base析构函数" << endl;
    }
};
class Derived : public Base // 公有派生
{
public:
    Derived()
    {
        cout << "Derived构造函数" << endl;
    }
    ~Derived()
    {
        cout << "Derived析构函数" << endl;
    }
};
int main()
{
    Base *p = new Derived(); // 使用基类指针指向用new创建的派生类对象
    delete p;
    return 0;
}

程序结果

Base构造函数
Derived构造函数
Base析构函数
请按任意键继续. . .

2)使用虚析构函数
#include <iostream>
#include <string>
using namespace std;
class Base // 基类
{
public:
    Base()
    {
        cout << "Base构造函数" << endl;
    }
    virtual ~Base() // 基类使用虚析构函数
    {
        cout << "Base析构函数" << endl;
    }
};
class Derived : public Base // 公有派生
{
public:
    Derived()
    {
        cout << "Derived构造函数" << endl;
    }
    ~Derived()
    {
        cout << "Derived析构函数" << endl;
    }
};
int main()
{
    Base *p = new Derived(); // 使用基类指针指向用new创建的派生类对象
    delete p;
    return 0;
}

程序结果

Base构造函数
Derived构造函数
Derived析构函数
Base析构函数
请按任意键继续. . .

Tip:只要基类的析构函数是虚函数,那么派生类的析构函数不论是否使用了virtual 关键字声明都自动成为虚析构函数。

如果一个类定义了虚函数,则最好将析构函数也定义为虚函数。【构造函数不能是虚函数】

4、纯虚函数和抽象类

1)纯虚函数

定义:

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,不能实例化对象,可以做指针和引用,拥有纯虚函数的类是抽象类。

作用:

在基类中为其派生类保留一个函数的名字,以便派生类根据需要对他进行定义。如果在基类中没有保留函数名字,则无法实现多态性。【它的实现留给该基类的派生类去做。】

声明格式
virtual  类型  函数名(参数列表)= 0
                 
virtual void bark() = 0;  //声明为纯虚函数

(1)纯虚函数没有函数体;
(2)最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”;
(3)这是一个声明语句,最后有分号。

2)抽象类

  • C++抽象类是为子类抽象一个基类,抽象类的主要作用是为子类提供相同的属性和方法,其他如果需要在子类中修改方法,需要将其声明为纯虚函数。
  • 含有一个及以上纯虚函数的类为抽象类
    需要注意:抽象类虽然不能创建自己的对象,但是可以有自己的指针

总结:
①抽象类就是含有纯虚函数
②抽象类不能有实例对象
③子类没有覆写所有的纯虚函数,则子类还是抽象类

class controller
{
	public:
		controller();
		void func() = 0; // 纯虚函数,意味着这是一个抽象类
	protected:
		int x;
		int y;
};
int main()
{
	controller MPCController; //错误,抽象类不能有自己的对象
	controller *p_controller; //正确,抽象类可以有自己的指针
}

5、虚基类

为了避免产生二义性,在派生类中,继承同一个间接基类的成员保留一个版本。

格式:

class 派生类名:virtual 派生方式 基类名{
   派生类体
}

# eg:
class A
class B:virtual public A
class C:virtual public A
class D:public B,public C

代码例子

#include <iostream>
#include <string>
using namespace std;
class A // 基类
{
public:
    string name;
    void printName()
    {
        cout << "基类A name:" << name << endl;
    }
};
class B : virtual public A // 对类A进行虚继承
{
public:
    string name2;
};
class C : virtual public A // 对类A进行虚继承
{
public:
    string name3;
};
// 派生类D的两个基类B、C具有共同基类A
// 使用虚继承,使类D的对象只包含类A的一个实例
class D : public B, public C
{
public:
    string name4;
};
int main()
{
    D obj;
    obj.name = "泰语小分队"; // 若不是虚继承,则会报错(有二义性)
    obj.name2 = "OCENASIDE";
    obj.printName(); // 若不是虚继承,则会报错(有二义性)
    cout << "obj.name2:" << obj.name2 << endl;
    return 0;
}

程序结果:

基类A name:泰语小分队
obj.name2:OCENASIDE
请按任意键继续. . .

七、输入/输出流

1、流

流是标准类库,用户程序中只能继承不能修改。

输出操作:是将一个对象转换成一个字符序列,输出到指定对象。
输入操作:是从某个对象接收到一个字符序列,然后将其转换为相应对象所要求的格式。

读操作在流数据抽象中被称为(从流中)“提取”,写操作被称为(向流中)“插入”。

1)iostream流类库的类关系


2)常用I/O流类列表

类名说明包含文件
抽象流基类ios流基类ios
输入流类istream通用输入流基类和其他输入流基类。cin 是该类的对象istream
ifstream文件输入流类。用于从文件读取数据fstream
输出流类ostream通用输出流基类和其他输出流基类。cout 是该类的对象ostream
ofstream文件输出流类。用于向文件写入数据fstream
输入输出流类iostream通用输入/输出流基类和其他输入/输出流基类istream
fstream文件输入/输出流类。既能从文件中读取数据,也能向文件中写入数据fstream

ifstream:该数据类型表示输入文件流,用于从文件读取信息。
ofstream:该数据类型表示输出文件流用于创建文件并向文件写入信息。
fstream:该数据类型通常表示文件流,且同时具有 ofstream 和ifstream 两种功能这意味着它可以创建文件,向文件写入信息,从文件读取信息。

ios::in 读文件不存在就无法打开
ios::out 写文件不存在则建立新文件文件存在则直接清空文件内容。

3)头文件

A) iostream

头文件 iostream 包含操作所有输入/输出流所需的基本信息。该文件含有 4 个标准流对象,提供了无格式化和格式化的I/O 功能。

B) iomanip

头文件iomanip 包含格式化I/O的带参数操纵符,可用于指定数据输入/输出的格式。

C) fstream

头文件 fstream 包含处理文件的有关信息,提供建立文件、读/写文件的各种操作接口。

2、标准流对象

流是一个抽象的概念,在进行实际 I/O 操作时必须将流与一种具体的物理设备连接起来。
标准流对象(也称为标准流)是为用户提供的常用外设与内存之同通信的通道,对数据进行解释和传输,提供必要的数据缓冲等。

1) iostream 4 个标准流对象

cin (标准输入流)

cin与标准输人设备(键盘)相关联,用于读取数据,可以被重定向为从文件中读取数据。

cout(标准输出流)

cout 与标准输出设备(显示器)相关联,用于输出数据可以被重定向为向文件里写入数据。

cerr(非缓冲错误输出流)

cerr与标准错误信息输出设备(显示器)相关联(非缓冲)用于输出出错信息,不能被重定向。

clog (缓冲错误输出流)

clog与标准错误信息输出设备相关联(缓冲),用于输出出错信息,不能被重定向。

cerr 和 clog 的区别在于:cerr 不使用缓冲区,直接向显示器输出信息;而输出到 clog 中的信息会先被存储到缓冲区中,缓冲区满或者刷新时才输出到屏幕。
重定向:就是改变默认的输入来源,或改变默认的输出目的地。

// 重定向函数 freopen 的原型如下:
FILE *freopen( const char *path, const char *mode, FILE *stream );

标准输出cout重定向到文件
#include <iostream>
#include <string>
using namespace std;
int main()
{
    int x, y;
    cin >> x >> y;
    // 将标准输出重定向到stream.txt 文件中3
    freopen("stream.txt", "w", stdout);
    if (x == 0)
        cerr << "error" << endl;
    else
        cout << x << "*" << y << "=" << x * y << endl;
    return 0;
};

程序结果:

在这里插入图片描述

标准输入重定向为文件
#include <iostream>
#include <string>
using namespace std;
int main()
{
    int x, count, sum = 0;
    // 将标准输入重定向到stream.dat 文件中
    freopen("stream.dat", "r", stdin);
    for (count = 0; count < 3; count++)
    {
        cin >> x;
        sum += x;
    }
    cout << "前三平均值:" << sum / 3 << endl;
    return 0;
};

程序结果:

在这里插入图片描述

2)ios 中错误状态字

标识常量含义
goodbit0X00流状态正常
eofbit0X01文件结束符
failbit0X02I/O 操作失败,数据未丢失,可以恢复
badbit0X04非法操作,数据丢失,不可恢复

(1)返回流是否结束
int eof() const;

当文件操作结束遇到文件尾时,函数返回1;否则返回0。在标准输入流 cin 中,可以通过按下(Ctl+Z) 组合键表示输入流的结束.

(2)返回流是否处于正常状态
int fail() const;

该函数返回 faibit 状态,以判断流操作是否失败。

(3)判断流是否正常
int good() const;
int operator void *():

如果 eofbit、ilbit 和 badbit 全部都没有被置位(即均为0),则返回1 (true),否则返回 0 (false)。

(4)返回流是否处于失败状态
int bad() const;
int operator void !():

只要 eofbit、failbit 和 badbit 中有一位被置位(即为 1),则返回1 (true),否则返回0 (false)。

(5)返回状态字
int rdstate() const:

该函数返回流的当前状态.

(6)恢复状态字
void clear(int nStata = 0);

该函数恢复或设置状态字。默认值为 0,即将流状态恢复为正常。

代码实例:

#include <iostream>
#include <string>
using namespace std;
int main()
{
    int x, count, sum = 0;
    // 将标准输入重定向到stream.dat 文件中
    freopen("stream.dat", "r", stdin);
    for (count = 0; count < 3; count++)
    {
        cin >> x;
        if(cin.eof()) break; //若输入流已经结束则退出
        else sum += x;
    }
    cout << "前三平均值:" << sum / 3 << endl;
    return 0;
};


3、控制I/O格式

C++进行I/O格式控制的方式一般有使用流操纵符、设置标志字和调用成员函数

1)流操纵符

常见数据类型的默认 I/O 格式
I/O的数据类型默认的输入格式默认的输出格式
short、 int、 long(signed、 unsigned)与整型常数相同一般整数形式,负数前面有负号
float、 double、 long double与浮点数相同浮点或指数格式,取决于哪个更短
char(signed、unsigned)第一个非空白字符单个字符(无引号)
char *(signed、unsigned)从第一个空白字符(空格、t、m等)开始到下一个空白字符结束字符序列(无引号)
void*无前缀的十六进制数无前缀的十六进制数
Bool将true 或1识别为 true,将 false 或0识别为 flase1或0

iostream 中常用流操纵符
流操纵符作用输入/输出
endl输出一个新行符,并清空流O
ends输出字符串结束,并清空流O
flush清空流缓冲区O
dec *以十进制形式输入或输出整数I/O
hex以十六进制形式输入或输出整数I/O
oct以八进制形式输入或输出整数I/O
ws提取空白字符O

iomanip常用的用于格式控制的流操纵符
流操纵符作用
fixed以普通小数形式输出浮点数
scientific以科学计数法形式输出浮点数
left左对齐,即在宽度不足时将填充字符添加到右边
right *右对齐,即在宽度不足时将填充字符添加到左边
setbase(int b)设置输出整数时的进制,b为8、10或16
setw(int w)指定输出宽度为 w 个字符,或输入字符串时读入 w 个字符。一次有效
setfill(int c)在指定输出宽度的情况下,输出的宽度不足时用 ASCI码为c的字符填充(默认情况是用空格填充)
setprecision(int n)设置输出浮点数的精度为n。在使用非 fixed 且非 scientific方式输出的情况下, n即为有效数字最多的位数。如果有效数字位数超过 n,则小数部分四舍五入,或自动变为科学计数法输出并保留一n位有效数字;在使用fixed 方式和 scientific方式输出的情况下,n是小数点后面应保留的位数
setiosflags( fimtfalgs f)通用操纵符。将格式标志f所对应的格式标志位置为 1
resetiosflags(fmtfalgs f)通用操纵符。将格式标志 f所对应的格式标志位置为0(清除)
boolapha把 true 和 false 输出为字符串
noboolalpha *把 tue 和 false 分别输出为1和0
showbase输出表示数值进制的前缀
noshowbase *不输出表示数值进制的前缀
showpoint总是输出小数点
noshowpoint *只有当小数部分存在时才显示小数点
showpos在非负数值中显示+
noshowpos *在非负数值中不显示+
skipws*输入时跳过空白字符
noskipws输入时不跳过空白字符
uppercase十六进制数中使用’A’~E。若输出前缓,则前级输出“0x”,科学计数法中输出E
no uppercase *十六进制数中使用a~e。若输出前缀,则前缀输出“0x”,科学计数法中输出e
internal数值的符号(正负号)在指定宽度内左对齐,数值右对齐,中间由填充字符填充

Tip:setbase、setw、setfill、setprecision、 setiosflags 和 resetiosflags 为函数形式的带有参数操纵符。
为方便使用,C++为输出流操纵符设置了默认值。例如,在默认情况下整数采用进制形式输出,等效于使用了流操纵符 dec。流操纵符与插入运算符“<<”和提取运算“>>”一起使用,从而方便格式控制。

代码例子
#include <iostream>    
#include <iomanip>    
using namespace std;    
int main()    
{    
    cout << setfill('@') << setw(4) << "RR" << setw(5) <<"lovely";
    return 0;
}

输出结果:

@@RRlovely请按任意键继续. . .

1)setw

setw(int n)用来控制输出间隔。【一共输出多少个字符】

2)setfill()

setfill()的作用是设置其后输出的内容的填充字符。 【若输出的内容不足setw间隔,则用setfill里面的内容填充】

2)标志字

通过 setiosflags() 设置标志字行格式控制的方式。

setiosflags()是带参数的操纵符,在头文件 iostream 中,用以设置指定的标志,函数的参数为流的格式标志位。标志字是一个 long 型的数据,由若千个系统定义的格式控制标志位“组合”而成。

常见格式标志常量及含义
标志常量名含义输入/输出
ios: :skipws0X0001跳过输入中的空白I
ios::left0X0002按输出域左对齐,用填充字符填充右边O
ios::right *0X0004按输出域右对齐,用填充字符填充左边O
ios::internal0X0008在符号位或基数指示符后填入字符O
ios::dec *0X0010转换为十进制基数形式I/O
ios::oct0X0020转换为八进制基数形式I/O
ios::hex0X0040转换为十六进制基数形式I/O
ios::showbase0X0080在输出中显示基数指示符O
ios::showpoint0X0100在输出浮点数时必须带小数点和尾部的 0O
ios::uppercase0X0200以大写字母表示十六进制数,科学计数法使用大写字母 EO
ios::showpos0X0400正数前加“+”号O
ios::scientific0X1000科学记数法显示浮点数O
ios::fixed0X1000定点形式表示浮点数O
ios::unitbuf0X2000插入操作后立即刷新流O

公有静态符号常量:
ios::basefield 值为 dec|oct|hex
ios::adjustifield 值为 left | right| internal
ios::floatfield 值为 scientific | fixed
【| 表示多个标志】

4、调用cout 的成员函数【osteam 类】

成员函数

(1)设置和返回标志字
long flags(long IFlags);

// 该函数返回当前的标志字。
long flags) const;

该函数的功能是使用参数 1Flags 设置(替换)标志字,返回值为设置前的标志字。

(2)设置标志位
long setf(long IFlags);

 long setf( long IFlags,long IMask);

该函数使用参数IFlags 置位指定的标志位,返回值为置位前的标志字。

(3)清除标志位
long unsetf(long IFlags);

该函数清除参数 IFlags 指定的标志位,返回清除前的标志字

(4)设置和返回输出宽度
int width(int nw);

//该函数返回值为当前的输出宽度值
int width()const;

该函数将下一个输出项的显示宽度设置为 nw。如果nw 大于数据所需宽度,则在没有特指示时数据采用右对齐方式。 小于数据所需宽度,则nw无效,数据以默认格式会出。函数 width0的设置没有持续性,输出一项数据后自动恢复为系统默认设置。

(5)设置填充字符
char fill(char cFill);
//该函数返回当前使用的填充符
char fill0 const;

当设置宽度大于数据输出需要的宽度时,空白位置以字符参数 Fill 填充。若数据在宽度域左对齐,则在数据右边填充;否则在左边填充。默认填充符为空白符。

(6)设置数据显示精度
int precision(int np);
//该函数返回当前数据显示精度。
int precision() const;

该函数用参数 np 设置数据显示精度。如果浮点数以定点形式输出,则 mp 表示小数点后的数字位数。如果设置为科学记数法输出,则np 为尾数精度位数(包括小数点)。系统默认的数据显示精度为6位。float 类型最大数据显示精度为 6位,double 类型最大数据显示精度为15位

ostream类的成员函数及与其作用相同的流操纵符

成员函数作用相同的流操纵符作用
precision(int np)setprecision(np)精度
width(int nw)setw(nw)输出宽度
fill(char cFill)setfill(cFill)填充字符
setf(long IFlags)setiosflags(IFlags)标志位
unsetf(long IFlags)resetiosflags(IFlags)清除标志位

ostream 类输出流的成员函数

(1)字符插入
ostream & put(char c);

成员函数 put0的功能是向输出流中插入一个字符 c。如果给出的参数类型为 int,则输该ASCII码对应的字符,函数返回输出流对象的引用。

(2)数据块插入
ostream & write(const char * pch, int nCount):

成员函数 write() 的功能是向输出流中插入 pch 指向的一个长度为 nCount 的字节序列。

5、调用cin 的成员函数 【istream 类】

成员函数

1.get() 函数
int get();

从输入流中读入一个字符(包括空白字符),返回值就是该字符的 ASCII 码。 如果碰到输入结束符,则返回值为系统常量 EOF (End Of File,文件结束标记)
可以采用 EOF 来判断输入是否结束。

Tip:输入行之后,再按(CtltZ)组合键和 (Enter) 键,cin.get()的返回值就是 EOF,使 while 循环正常退出。
在 Windows 环境下,当进行键盘输入时,在单独的一行按《CtlZ)组合键后再按(Enter)键就代表文件输入结束。在UNIX/Linux系统中,(Ctl+D)组合键代表输入文件结束。

2.getline()函数
istream & getline(char * buf, int bufSize);

是从输入流中的当前字符开始读取 bufSize-1 个字符到缓冲区 buf,或读到‘\n’为止(哪个条件先满足即按哪个执行)。函数会在 buf 中读入数据的结尾自动添加串结束标记‘\0’。

istream & getline(char * buf, int bufSize, char delim);

其功能是从输入流中的当前字符开始读取 bufSize-1 个字符到缓冲区 buf,或读到字符 delim 为止(哪个条件先满足即按哪个执行)。函数会在 buf 中读入数据的结尾自动添加‘\0’。

两者的区别在于,前者是读到“\n”为止,后者是读到指定字符 delim 为止。学符‘n’或delim 都不会被存入buf中,但会从输入流中取走。

3.eof()函数
bool eof();

判断输入流是否已经结束。返回值为 true 表示输入结束

4.ignore()函数
istream & ignore(int n=1, int delim = EOF);

跳过输人流中的n个字符或跳过 delim及其之前的所有字符。

5.peek()函数
int peek();

两数 peek()返回输入流中的当前字符,但是并不将该字符从输入流中取走。cin.peek()不会跳过输入流中的空格和回车符。在输入流已经结束的情况下,cin.peek()返回 EOF。

八、文件操作

1、文件基本概念和文件流

1)文件

文件:将一些内容相关的信息组织起来保存在一起并用一个名字标识。

1、根据文性数据的编码方式不同分为文本文件和二进制文件。
2、根据存取方式不同分为顺序存取文件和随机存取文性。

“读文件”:将文件中的数据读入内存之中,也称为“输入”。
“写文件”:将内存中的数据存入文件之中,也称为“输出”。

2)文件流

流是一个逻辑概念,是对所有外部设备的逻辑抽象。

文件流类

1)ifstream

用于从文件中读取数据。

2)ofstream

用于向文件中写入数据。

3) fstream

既可用于从文件中读取数据,又可用于向文件中写入数据

使用这3个流类时,程序中需要包含 fstream头文件。

3)使用文件

打开 (open)文件 —— 操作文件关闭 (close) 文件。

打开文件 open()

1)建立关联
2)指明文件的使用方式和文件格式。
打开文件 (两种方式)
A)先建立流对象,然后调用 open()函数连接外部文件。

流类名 对象名;
对象名.open(文件名,模式);

例子:

ifstream inFile; //建立输入文件流对象
inFile.open("data.txt", ios::in);  //连接文件,指定打开文件 【没有指定打开模式,默认以方式打开文本文件】

##OR
ifstream outFile; //建立输入文件流对象
outFile.open("c\\file", ios::out|ios::binary); //连接文件,指定打开模式

B)调用流类带参数的构造函数,在建立流对象的同时连接外部文件。

流类名 对象名(文件名,模式);

例子:

ifstream inFile("data.txt", ios::in);  

##OR
ifstream outFile("c\\file", ios::out|ios::binary); 

“文件名”是用字符串标迟的外部文件名称,可以是已经赋值的字符串,也可以是字符串常量。可以是包含完整路径的绝对文件名,也可以是只包含相对路径的文件名。
“模式”是类ios 定义的打开模式标记常量,用来表示文件的打开方式。

文件打开模式标记
模式标记适用对象作用
ios::inifstream
fstream
以读方式打开文件。如果文件不存在,则打开出错
ios::outofstream
fstream
以写方式打开文件。如果文件不存在,则新建该文件;如果文件已经存在,则打开时清除原来的内容
ios::appofstream以追加方式打开文件,用于在文件尾部添加数据。如果文件不存在,则新建该文件
ios::ateofstream打开一个已有的文件,并将文件读指针指向文件末尾。如果文件不存在,则打开出错
ios::truncofstream删除文件现有内容。单独使用时与 ios::out 相同
ios::binaryifstream
ofstream
fstream
以二进制方式打开文件。若不指定此模式,则以默认的文本模式打开文件
ios::in | ios::outfstream打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错
ios::in | ios::outofstream打开己存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错
ios::in | ios::out | ios::truncfstream打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件

关闭文件 close()

发出关闭文件命令后,系统会将缓冲区中的数据完整地写入文件,同时添加文件结束标记,切断流对象与外部文件的连接。

ifstream infile;
inFile.open("file.txt", ios::in);  //打开文件"file.txt"
//如果打开文件成功则进行读文件,此外可以编写相应的语句
inFile.close();//关闭文件
//到此流对象inFile 已经不再与"filetxt”相关联,可以用于其他文件
inFile.open("file.data", ios::in); //重用流对象,连接另一个文件"file.data"

代码例子:

#include <iostream>
#include <fstream>
using namespace std;
int main()
{
    ifstream inFile;
    inFile.open("fileData.txt", ios::in); // 以读的方式打开文本文件
    if (inFile)                           // 打开成功,inFile值为true
    {
        cout << "成功打开文件fileData.txt" << endl;
        inFile.close(); // 关闭文件
    }
    ofstream outFile;
    outFile.open("E:\\mein\\my\\c++\test\\testData.txt\n", ios::out | ios::in); // 已写的方式打开文本文件
    if (!outFile)
        cout << "error" << endl;
    else
    {
        cout << "成功打开文件test.txt";
        inFile.close(); // 关闭文件
    }
    
    ifstream inFile1("E:\\mein\\my\\c++\test\\testData.txt\n", ios::in); // 声明对象inFile 并调用构造函数
    if (inFile)
    {
        cout << "构造函数 成功打开文件test.txt";
        inFile.close(); // 关闭文件
    }
    else
        cout << "构造函数 失败打开文件test.txt";
    return 0;
};

程序结果:

成功打开文件fileData.txt
error
构造函数 成功打开文件test.txt请按任意键继续. . .

2、文件读写

1)文本文件读写

使用文件流对象打开文件后,文件就成为一个输入流或输出流。对于文本文件,可以用cin、cout 进行读写,在标准输入/输出流中可以使用的成员函数及流操纵符同样适用于文件流。

文本文件在存储数据时是以ASCI码形式保存数据的,文本方式存储信息时占用的空间较大,而且在进行数据输入/输出时还要额外进行内存和外存之间的数据格式转换。

代码例子
#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
int main()
{
    char name[10], address[30];
    // 文件写
    ofstream outFile;
    outFile.open("name.txt", ios::out); // 以写的方式打开文本文件
    if (!outFile)
    {
        cout << "创建文件失败" << endl;
        return 0;
    }
    cout << "请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)\n";
    while (cin >> name >> address)
        outFile << name << " " << address << endl; // 向流中插入数据
    outFile.close();
    // 文件读
    ifstream inFile;
    inFile.open("name.txt", ios::in); // 以读的方式打开文本文件
    if (!inFile)
    {
        cout << "打开文件失败" << endl;
        return 0;
    }
    cout << "读取文件内容\n";
    while (inFile >> name >> address)
        cout  << left << setw(9) << name << " " << setw(29) << right << address << endl;
    inFile.close();
    return 0;
};

程序结果:

请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)
泰语小分队 广州
OCEANSIDE 韶关
小蓉蓉 广州
^Z
读取文件内容
泰语小分队                          广州
OCEANSIDE                          韶关
小蓉蓉                             广州
请按任意键继续. . .

image.png

2)二进制文件读写

二进制数据文件以基本类型数据的二进制形式存放,即二进制文件中数据的存储格式内存格式一致,存储长度仅与数据类型相关。二进制数据流不会对写入或读出的数据做格式转换,便于高速处理数据。

用 binary方式打开一进制文件,调用 ifstream或 fstream的read()成员函数从文件中读取数据,调用ofstream或fstream的write()成员函数向文件中写入数据

1、用ostream::write() 成员函数写文件
// 原型
ostream & write(char * buffer, int nCount);

该成员函数将内存中 buffer 所指向的 nCount 个字节的内容写入文件,返回值是对函数所作用的对象的引用,如 obj.write(…)的返回值就是对 obj 的引用。该函数是非格式化操作将buffer 所指的数据按字节序列直接存入文件中。

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 姓名信息类
class CName
{
public:
    char name[10];
    char address[30];
};
int main()
{
    CName cn; // 创建对象
    //  文件写
    ofstream outFile("name.dat", ios::out | ios::binary); // 以二进制写方式打开文件
    // ofstream outFile("name.dat", ios::app | ios::binary); // 以二进制追加方式打开文件【ios::app】
    if (!outFile)
    {
        cout << "创建文件失败" << endl;
        return 0;
    }
    cout << "请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)\n";
    while (cin >> cn.name >> cn.address)
        outFile.write((char *)&cn, sizeof(cn)); // 向文件中写入数据
    outFile.close();
    return 0;
};

程序结果:

请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)
泰语小分队 广州
^Z
请按任意键继续. . .

在这里插入图片描述

2、用istream::read()成员函数读文件
// 原型
istream &read(char * buffer, int nCount);

该成员函数从文件中读取 nCout 个字节的内容,存放到 buffer 所指向的内存缓冲区中,返回值是对函数所作用的对拿的引用,该函数是非格式化操作,对读取的字节序列不进行处理,直接存入 buffer 中,由程序的类型定义解释。

3、用ostream::gcount()成员函数得到读取字节数

在read() 函数执行后立即调用文件流对象的成员函数gcount() ,其返回值是最近一次read() 函数执行时成功读取的字节数。

// 原型
int gcount();

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 姓名信息类
class CName
{
public:
    char name[10];
    char address[30];
};
int main()
{
    CName cn; // 创建对象
    //  文件写
    ofstream outFile("name.dat", ios::out | ios::binary); // 以二进制写方式打开文件
    if (!outFile)
    {
        cout << "创建文件失败" << endl;
        return 0;
    }
    cout << "请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)\n";
    while (cin >> cn.name >> cn.address)
        outFile.write((char *)&cn, sizeof(cn)); // 向文件中写入数据
    outFile.close();
    // 文件读
    int count = 0, nbyte = 0;                           // count条数 nbyte字节
    ifstream inFile("name.dat", ios::in | ios::binary); // 以二进制读方式打开
    if (!inFile)
    {
        cout << "打开文件失败" << endl;
        return 0;
    }
    cout << "读取文件内容\n";
    while (inFile.read((char *)&cn, sizeof(cn)))
    {
        cout << left << setw(9) << cn.name << " " << setw(29) << right << cn.address << endl;
        count++;                  // 每条数量叠加
        nbyte += inFile.gcount(); // 本次read() 读取的字节数量
    }
    cout << "一共内容数:" << count << ",字节数:" << nbyte << endl;
    inFile.close();
    return 0;
};

程序结果:

请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)
泰语小分队 广州
OCEANSIDE 韶关
^Z
读取文件内容
泰语小分队广州                          广州
OCEANSIDE                          韶关
一共内容数:2,字节数:80
请按任意键继续. . .

3)用成员函数put() 和 get() 读写文件

用类ifstream 和类 fstream 的成员函数 get() 从文件中一次读取一个字节。
用类ofstream和类 fstream 的成员函数 put() 向文件中一次写入一个字节。

函数get0有3种主要形式:

1、int get();

不带参数的 get() 函数从指定的输入流中提取一个字符(包含空白字符),函数的返回值即为该字符。当遇到文件结束符时,返回系统常量 EOF

2、 istream& get( char &rch);

从指定输入流中提取一个字符 (包含空白字符),将该字符作为 rch 引用的对象。当遇到文件结束符时,函数返回0;否则返回对 istream对象的引用。

3、istream& get( char *pch, int nCount, char delim=‘n’ );

从流的当前字符开始,读取 nCount-1 个字符,或遇到指定的分隔符 delim 结束。函数把读取的字符(不包括分隔符) 写入数组 pch 中,并在字符串后添加结束符"\0"。

函数put()
ostream& put(char ch)

向输出流中插入一个字节

4)文本文件与二进制文件异同

文本文件:

文本文件是以文本形式存储数据,其优点是具有较高的兼容性。缺点是存储一批纯数值信息时,要在数据之间人为地添加分隔符。在输入/输出过程中,系统要对内外存的数据式进行相应转换,文本文件的另一个缺点是不便于对数据进行随机访问。

纯文本信息(如字符串) 以文本文件形式存储。

二进制文件:

二进制文件是以二进制形式存储数据,其优点是便于对数据实行随机访问(相同数据型的数据所占空间的大小均是相同的,不必在数据之间人为地添加分隔符)。在输入/输出程中,系统不需要对数据进行任何转换。缺点是数据兼容性差。

数值信息以二进制文件形式存储。
用二进制方式打开文件总是比较安全的策略。

3、随机访问文件

顺序文件:
一个文件只能进行顺序存取操作。

键盘、显示器和保存在磁带上的文件。
顺序文件只能进行顺序访问。

随机文件:
一个文件可以在文件的任意位置进行存取操作。

磁盘文件。
随机文件既可以进行顺序访问,也可以进行随机访问。

可以通过成员函数改变文件指针的位置,使程序可以在文件任意位置读/写数据,对文件中数据随机访问。

seekg() 读指针

在类istream、类ifstream和类fstream中seekg()可以设置文件读指针的位置。
在类ostream、类ofstream和类fstream中seekg()可以设置文件写指针的位置。

类istream

(1)移动读指针函数
istream & seekg(long pos);

将读指针设置为pos,即将读指针移动到文件的 pos 字节处

istream & seekg(long offset, ios::seek dir dir);

将读指针按照 seek_dir 指示(方向)移动offset 个字节,其中seek dir 是在类ios中定义的一个枚举类型。

seek dir 的常量值:
1、ios::beg:表示流的开始位置。此时,offset 应为非负整数。
2、ios:cur:表示流的当前位置。此时,ofset 为正数则表示向后(文件尾)移动,为负则表示向前(文件头入移动。
3、ios::end:表示流的结束位置。此时,offiset 应为非正整数。

inFile.seekg(20, ios::beg);  //以流开始位置为基准,后移 20 个字节
inFile.seekg(-10, ios:cur);  //以流指针当前位置为基准,前移 10 个字节
inFile.seekg(-10,ios::end);  //以流结尾位置为基准,前移 10个字节

 seekg(n)等价于: seekg(n,ios::beg);

(2)返回读指针当前位值的函数
long tellg();

函数返回值为流中读指针的当前位置

类ostream

(1)移动写指针函数
ostream & seekp(long pos);
##OR
ostream & seekp(long offset, ios::seek dir dir);

(2)返回写指针当前位置的函数
long tellp();

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 姓名信息类
class CName
{
public:
    char name[10];
    char address[30];
};
int main()
{
    CName cn; // 创建对象
    //  文件写
    ofstream outFile("name.dat", ios::out | ios::binary); // 以二进制写方式打开文件
    if (!outFile)
    {
        cout << "创建文件失败" << endl;
        return 0;
    }
    cout << "请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)\n";
    while (cin >> cn.name >> cn.address)
        outFile.write((char *)&cn, sizeof(cn)); // 向文件中写入数据
    outFile.close();
    // 文件读
    int count = 0, nbyte = 0;                           // count条数 nbyte字节
    ifstream inFile("name.dat", ios::in | ios::binary); // 以二进制读方式打开
    if (!inFile)
    {
        cout << "打开文件失败" << endl;
        return 0;
    }
    else
    {
        cout << "打开文件时的位置:" << inFile.tellg() << endl;
    }
    cout << "读取文件内容\n";
    while (inFile.read((char *)&cn, sizeof(cn)))
    {
        cout << left << setw(9) << cn.name << " " << setw(29) << right << cn.address << endl;
        count++;                  // 每条数量叠加
        nbyte += inFile.gcount(); // 本次read() 读取的字节数量
    }
    cout << "读取文件结束时位置指针:" << inFile.tellg() << endl;
    cout << "一共内容数:" << count << ",字节数:" << nbyte << endl;
    inFile.clear(); //将流恢复正常状态
    inFile.seekg(0); // 将文件读指针移动到文件起始位置
    cout << "位置指针:" << inFile.tellg() << endl;
    inFile.read((char *)&cn, sizeof(cn)); // 读文件
    cout << left << setw(9) << cn.name << " " << setw(29) << right << cn.address << endl;
    inFile.seekg(0, ios::end); // 将文件读指针移动到文件最后位置
    cout << "位置指针:" << inFile.tellg() << endl;
    inFile.close();
    return 0;
};

程序结果:

请输入姓名 地址(回车 以Ctrl+Z结束输入 再回车)
泰语小分队 广州
OCEADSIDE 韶关
小蓉蓉 广州
圣诞老人 韶关
^Z
打开文件时的位置:0
读取文件内容
泰语小分队广州                          广州
OCEADSIDE                          韶关
小蓉蓉                             广州
圣诞老人                           韶关
读取文件结束时位置指针:-1
一共内容数:4,字节数:160
位置指针:0
泰语小分队广州                          广州
位置指针:160
请按任意键继续. . .

九、函数模板和类模板

泛型:最一般化的类型。使用泛型的算法只要实现一遍,就能适用于多种数据类型。能够减少重复代码的编写。模板是创建泛型类或函数的蓝图或公式。

1、函数模板

解决函数中参数的类型有差异,但是实现的功能类似。实现代码的复用。

函数在设计时并不使用实际的类型,而是使用虚拟的类型参数。实例化时传递参数类型。

格式

声明
// 1)同类型参数
template<模板参数表>
返回类型名 函数模板名(参数表)
{
    函数体的定义
}
#eg
template<class T>
void print(T name){
    cout<<name<<endl;
}

// 2)不同类型参数
template<模板参数表1,模板参数表2...>
返回类型名 函数模板名(参数表1,参数表2...)
{
    函数体的定义
}
#eg
template<class T,class T1>
void print(T name,T1 age){
    cout<<name<<age<<endl;
}

模板参数表:如果是一个类型,则需要使用 typename 或class 天键字来表示参数的类型;如果参数是一个值,则需要写这个值的类型。

类型 参数名

1)class(或 typename) 标识符,指明函数模板中可以接收的类型参数,类型参数由标识符表示。
2)类型说明符 标识符,指明函数模板中可以接收一个由“类型说明符”所规定类型的常量作为参数。

使用
模板名 <实际类型参数 1,实际类型参数2,...>
#eg:
int age=18;
string name="泰语小分队";
// 同类型一个参数
print(name);
// 不同类型两个参数
print(name,age);

代码实例:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 绝对值函数模板
template <typename T>
T abs(T x)  // 函数返回类型也是T
{
    return x < 0 ? -x : x;
}
// 多个参数函数模板
template <typename T, class T1> // 参数列表标识符 class和typename 都可以
void print(T name, T1 age)
{
    cout << "姓名:" << name << ",年龄" << age << endl;
}
int main()
{
    int age = 18;
    string name = "泰语小分队";
    int a = 20;
    double b = -5.5;
    float c = 3.14;
    cout << "绝对值函数模板:" << endl;
    cout << "a:" << abs(a) << endl;
    cout << "b:" << abs(b) << endl;
    cout << "c:" << abs(c) << endl;
    cout << "多个参数函数模板:" << endl;
    print(name, age);
    return 0;
};

程序结果:

绝对值函数模板:
a:20
b:5.5
c:3.14
多个参数函数模板:
姓名:泰语小分队,年龄18
请按任意键继续. . .

函数模板和函数的区别:

1)函数模板本身在编译时不会生成任何目标代码,只有当通过模板生成具体的函数实例时才会生成目标代码。
2)被多个源文件引用的函数模板,应当连同函数体一同放在头文件中,而不能像普通函数那样只将声明放在头文件中。
3)数指针也只能指向模板的实例,而不能指向模板本身

函数模板和函数匹配顺序:

函数与函数模板也是允许重载的。

1) 先找参数完全匹配的普通函数 (不是由模板实例化得到的模板函数)。
2)再找参数完全匹配的模板函数
3)然后找实参经过自动类型转换后能够匹配的普通函数
4)如果上面的都找不到,则报错

2、类模板

以使用泛型数据类型替代实际的数据类型来说明成员变量,从而定义一个类。这相当于定义一个类的模板。

为类定义一种模式,使得类中的某些成员变量、默认成员函数参数、某些成员函数的返回值及局部变量能取任意类型,既可以是系统预定义的类型,也以是用户自定义的类型。

格式

声明
template<模板参数表>
class 函数模板名
{
    类体的定义
   // 1)类模板的成员函数在类体内
   返回类型名 成员函数名(参数表)
   {
       函数体
   }
}

// 2)类模板的成员函数在类体外
template<模板参数表>
返回类型名 类模板名<模板参数标识符列表>::成员函数名(参数表)
{
    函数体
}

使用
类模板名 <模板参数表> 对象名;
##OR
类模板名 <模板参数表> 对象名 1(构造函数实参),...,对象名n(构造函数实参);

实例化类模板,声明对象

代码例子

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
template <class T1, class T2>
class NameInfo
{
public:
    T1 name;    // 第一个值
    T1 address; // 第二个值
    T2 age;     // 第三个值
    NameInfo(T1 n, T1 adr, T2 ag) : name(n), address(adr), age(ag)
    {
        cout << "姓名:" << name << ",地址:" << address << ",年龄:" << age << endl;
    }
    void print()
    {
        cout << "类模板成员函数" << endl;
    }
};
int main()
{
    NameInfo<string, int> nf("泰语小分队", "广州", 18);
    nf.print();
    return 0;
};

程序结果:

姓名:泰语小分队,地址:广州,年龄:18
类模板成员函数
请按任意键继续. . .

类模板使用静态成员

template <typename T> 
class TestClass{
public:
    static T a; //类模板中静态成员
    static void print(){
       a += 1;
    }
}

使用普通参数的类模板

// string n
template <string n> 
class TestClass{
public:
    string name = n;
}

类模板使用函数模板

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
template <class T1, class T2>
class NameInfo
{
public:
    T1 name;    // 第一个值
    T1 address; // 第二个值
    T2 age;     // 第三个值
    NameInfo(T1 n, T1 adr, T2 ag) : name(n), address(adr), age(ag)
    {
        cout << "姓名:" << name << ",地址:" << address << ",年龄:" << age << endl;
    }
    void print()
    {
        cout << "类模板成员函数" << endl;
    }
    // 函数模板
    template <typename T>
    void printName(T t)
    {
        cout << "类模板的函数模板:" << t << endl;
    }
};
int main()
{
    NameInfo<string, int> nf("泰语小分队", "广州", 18); //类模板实例化并声明对象
    nf.print(); // 调用类模板成员函数
    nf.printName("终于要看完啦!!!"); //nf.printName("终于要看完啦!!!"); == nf.printName<string>("终于要看完啦!!!")
    return 0;
};

类模板与继承

1)普通类继承模板类
2)类模板继承普通类
3)类模板继承类模板
4)类模板继承模板类

1)普通类继承模板类

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 类模板 基类
template <class T>
class TBase
{
    // T name;

public:
    void printName()
    {
        cout << "类模板基类:"  << endl;
    }
};
// 从模板类继承 普通类
class Derived : public TBase<string>
{
};
int main()
{
    Derived d;     // 普通派生类对象
    d.printName(); // 调用类模板中成员函数
    return 0;
};

程序结果:

类模板基类:
请按任意键继续. . .

2)类模板继承普通类

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 普通类 基类
class TBase
{

public:
    void printName()
    {
        cout << "基类" << endl;
    }
};
// 从模板类继承 普通类
template <class T>
class TDerived : public TBase
{
    T name;

public:
    void setValue(T n)
    {
        name = n;
    }
    void print()
    {
        TBase::printName();
        cout << "类模板 派生类name:" << name << endl;
    }
};
int main()
{
    TDerived<string> d;       // 类模板实例化 声明对象
    d.setValue("要开心鸭!!"); // 调用类模板中成员函数
    d.print();
    return 0;
};

程序结果:

基类
类模板 派生类name:要开心鸭!!
请按任意键继续. . .

3)类模板继承类模板

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 类模板 基类
template <class T>
class TBase
{
    T address;

public:
    void setAdr(T adr)
    {
        address = adr;
    }
    void printName()
    {
        cout << "类模板 基类address:" << address << endl;
    }
};
// 类模板  派生类
template <class T1,class T2>
class TDerived : public TBase<T1>
{
    T2 name;

public:
    void setValue(T2 n)
    {
        name = n;
    }
    void print()
    {
        TBase<T1>::setAdr("广州");
        TBase<T1>::printName();
        cout << "类模板 派生类name:" << name << endl;
    }
};
int main()
{
    TDerived<string,string> d;         // 类模板实例化 声明对象
    d.setValue("2023年要开心鸭!!"); // 调用类模板中成员函数
    d.print();
    return 0;
};

程序结果:

类模板 基类address:广州
类模板 派生类name:2023年要开心鸭!!
请按任意键继续. . .

4)类模板继承模板类

代码例子:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
// 类模板 基类
template <class T>
class TBase
{
    T address;

public:
    void setAdr(T adr)
    {
        address = adr;
    }
    void printName()
    {
        cout << "类模板 基类address:" << address << endl;
    }
};
// 类模板  派生类
template <class T2>
class TDerived : public TBase<string> // 类模板继承于模板类【参数类型确定的类模板TBase<string>】
{
    T2 name;

public:
    void setValue(T2 n)
    {
        name = n;
    }
    void print()
    {
        TBase<string>::setAdr("广州");
        TBase<string>::printName();
        cout << "类模板 派生类name:" << name << endl;
    }
};
int main()
{
    TDerived<string> d;               // 类模板实例化 声明对象
    d.setValue("2022年完结啦!!!"); // 调用类模板中成员函数
    d.print();
    return 0;
};

程序结果:

类模板 基类address:广州
类模板 派生类name:2022年完结啦!!!
请按任意键继续. . .

其他问题总结

1、c++ 预处理

1)定义

C++预处理指的是C++编译器在接受到源文件后,在编译期之前对程序进行的处理操作。

2)特点

C++预处理不会对程序进行词法解析,其以直接的替换为主,方便编译器的后续操作。

3)基本格式

一般的预处理指令格式为
#directive tokens
#符号应是这一行的第一个非空字符,一般我们把它放在起始位置。
directive 表示指令名称,tokens 的功能类似函数中的参数
如果指令一行放不下,可以通过\进行续行,例如:

#define Error if(error) exit(1)
// 等价于
#define Error \
if(error) exit(1)

2、new

new 是C++的一个操作符,函数返回值需要是系统原有的类型或者自己创建的类

注:本博客仅供学习和参考,内容较多,难免会有错误,请大家多多包涵。如有侵权敬请告知。
参考资料《C++程序设计》辛运帏、陈朔鹰编著

欢迎您在评论区留下您宝贵的建议
2022年完结撒花~~~🌸🌸

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的二维数组是一个数组的数组,也就是说它包含多个一维数组,每个一维数组又包含多个元素。 定义二维数组 Java中定义二维数组的通用语法如下: 数据类型[][] 数组名 = new 数据类型[行数][列数]; 例如,定义一个3行4列的二维数组intArray: int[][] intArray = new int[3][4]; 赋值和访问二维数组元素 二维数组的元素可以通过行列索引来访问和修改。例如,可以使用以下语句给第2行第3列的元素赋值为10: intArray[1][2] = 10; 可以使用以下语句访问第1行第2列的元素的值: int value = intArray[0][1]; 遍历二维数组 可以使用嵌套循环来遍历二维数组的所有元素。例如,以下代码可以输出二维数组intArray的所有元素: for (int i = 0; i < intArray.length; i++) { for (int j = 0; j < intArray[i].length; j++) { System.out.print(intArray[i][j] + " "); } System.out.println(); } 输出结果为: 0 0 0 0 0 0 10 0 0 0 0 0 二维数组的应用 二维数组常用于表示矩阵、地图等二维数据结构。例如,以下代码使用二维数组表示一个3x3的井字棋游戏: char[][] ticTacToe = new char[3][3]; 可以使用以下代码将井字棋棋盘初始化为空格: for (int i = 0; i < ticTacToe.length; i++) { for (int j = 0; j < ticTacToe[i].length; j++) { ticTacToe[i][j] = ' '; } } 可以使用以下代码将井字棋棋盘输出到控制台: for (int i = 0; i < ticTacToe.length; i++) { for (int j = 0; j < ticTacToe[i].length; j++) { System.out.print(ticTacToe[i][j] + " "); } System.out.println(); } 输出结果为:                   可以使用以下代码在第2行第2列落子: ticTacToe[1][1] = 'X'; 可以使用以下代码在第1行第3列落子: ticTacToe[0][2] = 'O'; 可以使用以下代码检查第1行是否已经有三个相同的棋子: if (ticTacToe[0][0] == ticTacToe[0][1] && ticTacToe[0][1] == ticTacToe[0][2]) { System.out.println(ticTacToe[0][0] + " wins!"); } 可以使用类似的方式检查其他行、列和对角线是否已经有三个相同的棋子。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值