C++面向对象程序设计

2023-09-04 14:35:49 浏览数 (2)

本文最后更新于 208 天前,其中的信息可能已经有所发展或是发生改变。

C 核心编程


1 内存分区模型

C 程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:

不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程


1.1 程序运行前


在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

(1) 代码区:

内容:存放CPU执行的机器指令

特点

  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
  • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

(2) 全局区:

内容:全局变量和静态变量存放在此.

特点

  • 全局区还包含了常量区, 字符串常量和其他常量也存放在此
  • 该区域的数据在程序结束后由操作系统释放

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

//全局变量 
int g_a=10;
int g_b=10; 

//静态全局变量
static int s_g_a=10;
static int s_g_b=10; 

//字符串全局常量
string g_s1="abcd";
string g_s2="abcd";

//const 修饰的全局常量 
const int g_c_a=10;
const int g_c_b=10;

int main(){

    cout<<"程序运行前:"<<endl;

    cout<<endl;

    //局部变量 
    int a=10;
    int b=10;

    //局部变量的地址
    cout<<"局部变量a的地址为:"<<&a<<endl;
    cout<<"局部变量b的地址为:"<<&b<<endl;

    //全局变量的地址
    cout<<"全局变量g_a的地址为:"<<&g_a<<endl;
    cout<<"全局变量g_b的地址为:"<<&g_b<<endl; 

    cout<<endl;

    //静态局部变量
    static int s_a=10;
    static int s_b=10; 

    //静态局部变量的地址 
    cout<<"静态局部变量s_a的地址为:"<<&s_a<<endl;
    cout<<"静态局部变量s_b的地址为:"<<&s_b<<endl;

    //静态全局变量的地址 
    cout<<"静态全局变量s_g_a的地址为:"<<&s_g_a<<endl;
    cout<<"静态全局变量s_g_b的地址为:"<<&s_g_b<<endl;

    cout<<endl;

    //常量
    //字符串常量
    string s1="abcd";
    string s2="abcd"; 

    //const 修饰的局部常量
    const int c_a=10;
    const int c_b=10; 

    //字符串局部常量的地址 
    cout<<"字符串局部常量s1的地址为:"<<&s1<<endl;
    cout<<"字符串局部常量s2的地址为:"<<&s2<<endl;

    //字符串全局常量的地址
    cout<<"字符串全局常量g_s1的地址为:"<<&g_s1<<endl;
    cout<<"字符串全局常量g_s2的地址为:"<<&g_s2<<endl;

    //const 修饰的局部常量
    cout<<"const 修饰的局部常量c_a的地址为:"<<&c_a<<endl;
    cout<<"const 修饰的局部常量c_b的地址为:"<<&c_b<<endl;

    //const 修饰的全局常量
    cout<<"const 修饰的全局常量g_c_a的地址为:"<<&g_c_a<<endl;
    cout<<"cosnt 修饰的全局常量g_c_b的地址为:"<<&g_c_b<<endl;

    cout<<endl;

    cout<<"有全局修饰的在全局区"<<endl;
    cout<<"其他的不在全局区"<<endl; 

    return 0;

}

总结

  • C 中在程序运行前分为全局区代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放 const修饰的全局常量 和 字符串常量

1.2 程序运行后


在程序编译后,生成了exe可执行程序,执行该程序后分为两个区域

(1) 栈区:

  • 由编译器自动分配释放, 存放函数的参数值,局部变量等

注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

(2) 堆区:

  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • C 中主要利用new在堆区开辟内存

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int* test_01(){
    int a=10;  //局部变量存储在栈区 
    return &a;  //不要返回局部变量的地址 
}

int* test_02(){
    int* m=new int(10);  //利用new将数据开辟到堆区 
    return m;
}

int main(){

    cout<<"栈区数据由编译器自动分配释放, 存放函数的参数值,局部变量等"<<endl;

    //调用函数test_01 
    int* p1=test_01();

    cout<<endl;

    //输出 
    cout<<"第一次输出,编译器对局部变量做一次保留,暂时不释放: "<<*p1<<endl;

    cout<<"第二次输出,编译器不再保留栈区的数据,直接释放:"<<*p1<<endl;

    cout<<"不要返回局部变量的地址!!!"<<endl;

    cout<<endl;

    //调用函数test_02
    int* p2=test_02();

    //输出
    cout<<"输出存放在堆区的数据,编译器不释放,由程序员手动释放: "<<*p2<<endl; 
    cout<<"输出存放在堆区的数据,编译器不释放,由程序员手动释放: "<<*p2<<endl; 
    cout<<"输出存放在堆区的数据,编译器不释放,由程序员手动释放: "<<*p2<<endl; 

    cout<<endl; 

    //释放堆中开辟的数据
    delete p2;

    cout<<"程序员手动释放后: "<<*p2<<endl; 

    return 0;

}

总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内存


1.3 new操作符


C 中利用new操作符在堆区开辟数据

​ 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符delete

语法 new 数据类型

​ 利用new创建的数据,会返回该数据对应的类型的指针

示例1: 开辟数据

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std; 

int* test_01(){
    int* a=new int(10);  //堆区开辟数据
    return a;
}

int main(){

    int *p=test_01();

    cout<<*p<<endl;
    cout<<*p<<endl;

    //利用delete释放堆区数据
    delete p;

    cout<<*p<<endl; //已释放,输出垃圾值 

    return 0;

}

示例2:开辟数组

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std; 

int* test_01(){
    int* a=new int[10];  //堆区中开辟数组 
    return a;
}

int main(){

    int *p=test_01();

    for(int i=0;i<10;i  ) p[i]=i 1;  //赋值 

    for(int i=0;i<10;i  ) cout<<p[i]<<" ";  //输出 

    cout<<endl;

    //未释放前输出p[0]
    cout<<*p<<endl; 

    //利用delete释放堆区数据
    delete[] p;

    //已释放,输出垃圾值 
    cout<<*p<<endl; 

    return 0;

}

2 引用及其使用


2.1 引用的基本使用


作用: 给变量起别名

语法: 数据类型 &别名 = 原名

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int main(){

    int a=10;

    int &b=a;  //创建a的别名为b 必须初始化 

    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;  //b的值同a 

    //修改a的值 
    a=20;

    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;  //b的值也发生改变 

    //修改b的值 
    b=10;

    cout<<"a = "<<a<<endl;  //a的值也发生改变 
    cout<<"b = "<<b<<endl;

    return 0;

}

2.2 引用注意事项


  • 引用必须初始化
  • 引用在初始化后,不可以改变

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int main() {

    int a=10;
    int b=20;
    //int &c; //错误,引用必须初始化
    int &c=a; //一旦初始化后,就不可以更改
    c=b; //这是赋值操作,不是更改引用

    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;

    return 0;

}

2.3 引用做函数参数


作用:函数传参时,可以利用引用的技术让形参修饰实参

优点:可以简化指针修改实参

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

void swap(int &a,int &b){
    int t=a;
    a=b;
    b=t;
}

int main(){

    int a=10;
    int b=20;

    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;

    cout<<endl;

    swap(a,b);

    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl; 

    return 0;

}

总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单


2.4 引用做函数返回值


作用:引用是可以作为函数的返回值存在的

注意不要返回局部变量引用

用法:函数调用作为左值

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int &test_01(){
    int a=10;  //局部变量 
    return a;
}

int &test_02(){
    static int a=10;  //全局区变量 
    return a;
}

int main(){

    int &ans_01=test_01();

    cout<<"ans_01 = "<<ans_01<<endl;  //第一次输出正确是因为编译器做了保留 
    cout<<"ans_01 = "<<ans_01<<endl;  //再次输出已经被释放,输出垃圾值
    cout<<"不要返回局部变量的引用!!!"<<endl; 

    cout<<endl;

    int &ans_02=test_02();

    cout<<"ans_02 = "<<ans_02<<endl;
    cout<<"ans_02 = "<<ans_02<<endl;
    cout<<"ans_02 = "<<ans_02<<endl;

    test_02()=20;  //函数调用作为左值 

    cout<<"ans_02 = "<<ans_02<<endl;

    return 0;

}

2.5 引用的本质


本质:引用的本质在c 内部实现是一个指针常量.

讲解示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

//发现是引用,转换为 int* const ref = &a;
void test_01(int& ref){
    ref=100; // ref是引用,转换为*ref = 100
}

int main(){

    int a=10;

    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
    int& ref=a; 
    ref=20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;

    cout<<"a = "<<a<<endl;
    cout<<"ref = "<<ref<<endl;

    test_01(a);

    cout<<"a = "<<a<<endl;
    cout<<"ref = "<<ref<<endl;

    return 0;

}

结论:C 推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了


2.6 常量引用


作用:常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加==const修饰形参==,防止形参改变实参

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int test_01(const int &a){
    int b=a 10;
//  a=b;  //报错 a被const修饰 不可修改 
    return b;
}

int main(){

    int a=10;

    int b=test_01(a);

    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;

    return 0;

}

3 函数提高


3.1 函数默认参数


C 中,函数的形参列表中的形参是可以有默认值的。

语法: 返回值类型 函数名 (参数= 默认值){}

示例:

代码语言:javascript复制
//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int add(int a,int b=10){
    return a b;
}

int main(){

    int a=20,b=30;

    cout<<add(a,b)<<endl;

    cout<<add(a)<<endl;  //未传入参数b 默认b=10 

    return 0;
}

3.2 函数占位参数


C 中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型){}

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

示例:

代码语言:javascript复制
//函数占位参数 ,占位参数也可以有默认参数
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int add(int a,int){
    return a;
}

int main(){

    int a=20,b=30;

    cout<<add(a,b)<<endl;  //占位参数必须填补

    return 0;
}

3.3 函数重载


3.3.1 函数重载概述

作用:函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同 或者 个数不同 或者 顺序不同

注意: 函数的返回值不可以作为函数重载的条件

示例:

代码语言:javascript复制
//函数重载需要函数都在同一个作用域下
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int add(){
    cout<<"add() 的调用: "<<0<<endl;
}

int add(int a){
    cout<<"add(int a) 的调用:"<<a<<endl;
}

int add(double a){
    cout<<"add(double a) 的调用:"<<a<<endl;
}

int add(int a,int b){
    cout<<"add(int a,int b) 的调用:"<<a<<" "<<b<<"="<<a b<<endl;
}

int add(int a,double b){
    cout<<"add(int a,double b) 的调用:"<<a<<" "<<b<<"="<<a b<<endl;
}

int add(double a,int b){
    cout<<"add(dpuble a,int b) 的调用:"<<a<<" "<<b<<"="<<a b<<endl;
}

int add(double a,double b){
    cout<<"add(dpuble a,double b) 的调用:"<<a<<" "<<b<<"="<<a b<<endl;
}

int main(){

    add();

    add(1);

    add(1.11);

    add(1,2);

    add(1,2.22);

    add(1.11,2);

    add(1.11,2.22);

    return 0;
}

3.3.2 函数重载注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认参数

示例:

代码语言:javascript复制
//1、引用作为重载条件
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

void func(int &a){
    cout<<"func(int &a) 的调用:"<<a<<endl;
}

void func(const int &a){
    cout<<"func(const int &a) 的调用:"<<a<<endl;
}

int main(){

    int a=10;

    func(a); //调用无const
    func(20);//调用有const

    return 0;
}
代码语言:javascript复制
//函数重载碰到函数默认参数
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

void func(int a, int b = 10)
{
    cout<<"func2(int a,int b = 10) 的调用"<<endl;
}

void func(int a)
{
    cout<<"func(int a) 的调用"<<endl;
}

int main(){

    func(10);  //报错,原因产生歧义

    return 0;

}

4 类和对象


C 面向对象的三大特性为:封装、继承、多态

C 认为万事万物都皆为对象,对象上有其属性和行为

例如:

​ 人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌...

​ 车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、放空调...

​ 具有相同性质的==对象==,我们可以抽象称为==类==,人属于人类,车属于车类


4.1 封装


4.1.1 封装的意义

封装是C 面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

封装意义一:

​ 在设计类的时候,属性和行为写在一起,表现事物

语法: class 类名{ 访问权限: 属性 / 行为 };

示例1:设计一个圆类,求圆的周长

示例代码:

代码语言:javascript复制
//1、封装的意义
//将属性和行为作为一个整体,用来表现生活中的事物

//封装一个圆类,求圆的周长
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

const double PI=3.1415926;

class circle{
    public:  //访问权限  公共的权限

        //属性
        int r;  //半径

        //行为
        double caculate(){  //计算圆的周长
            return r*r*PI;
        }

};

int main(){

    //通过圆类,创建圆的对象
    circle c1;  // c1就是一个具体的圆

    c1.r=10;  //给圆对象的半径 进行赋值操作

    cout<<"c1的周长为: "<<c1.caculate()<<endl;

    return 0;

}

封装意义二:

类在设计时,可以把属性和行为放在不同的权限下,加以控制

访问权限有三种:

  1. public 公共权限
  2. protected 保护权限
  3. private 私有权限

示例:

代码语言:javascript复制
//三种权限
//公共权限  public     类内可以访问  类外可以访问
//保护权限  protected  类内可以访问  类外不可以访问
//私有权限  private    类内可以访问  类外不可以访问
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:  //访问权限  公共的权限

        //姓名  公共权限
        string name;

    protected:

        //钱  保护权限
        int money;

    private:

        //年龄 私有权限 
        int year;

    public:
        void make(){  //初始化 
            name="lys";
            money=100000000;
            year=20;
        }

};

int main(){

    //通过persoin类,创建对象p1 
    person p1;

    p1.make();  //初始化p1 

    cout<<p1.name;  //public 类外可以访问 

    p1.money=0;  // protected 类外不可更改,不可访问 
    cout<<p1.money; 

    p1.year=100;  //private 类外不可更改,不可访问 
    cout<<p1.year;   

    return 0;

}

4.1.2 struct和class区别

C 中 struct和class唯一的区别就在于 默认的访问权限不同

区别:

  • struct 默认权限为公共
  • class 默认权限为私有
代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

struct point_1{
    int x,y;  //默认是公共权限
};

class point_2{
    int x,y;  //默认是私有权限
};

int main(){
    point_1 p1;
    point_2 p2;

    p1.x=10,p1.y=20;

    p2.x=10,p2.y=20;  //错误,访问权限是私有

    return 0;
}

4.2 对象的初始化和清理


4.2.1 构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

​ 一个对象或者变量没有初始状态,对其使用后果是未知

​ 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

解决方法

c 利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供

编译器提供的构造函数和析构函数是空实现。

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
  5. 不能设为私有
代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        //构造函数
        point(){
            cout<<"point的构造函数调用"<<endl;
        }

        //析构函数
        ~point(){
            cout<<"point的析构函数调用"<<endl;
        }

};

int main(){

    point p1;

    return 0;

}

4.2.2 构造函数的分类及调用

两种分类方式:

​ 按参数分为: 有参构造和无参构造

​ 按类型分为: 普通构造和拷贝构造

三种调用方式:

​ 括号法

​ 显示法

​ 隐式转换法

示例:

代码语言:javascript复制
//1、构造函数分类
// 按照参数分类分为 有参和无参构造   无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:

        int x,y;

        point(){
            cout<<"无参构造函数调用"<<endl; 
        }

        point(int a,int b){
            x=a;
            y=b;
            cout<<"有参构造函数调用"<<endl;
        }

        point(const point &p){
            x=p.x;
            y=p.y;
            cout<<"拷贝构造函数调用"<<endl;
        }

};

int main(){

    //括号法,常用
    point p1;  //调用无参构造函数
    point p2(1,2);  //调用有参构造函数
    point p3(p2);  //调用拷贝构造函数 

    //注意不可point p1()加括号,否则编译器会认为是函数声明而不是构造 

    point p4=point();  //调用无参构造函数
    point p5=point(3,4);  //调用有参构造函数
    point p6=point(p5);  //调用拷贝构造函数 

    point p7;  //调用无参构造函数
    point p8={5,6};  //调用有参构造函数
    point p9=p8;  //调用拷贝构造函数 

    return 0;

}

4.2.3 拷贝构造函数调用时机

C 中拷贝构造函数调用时机通常有三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:

        int x,y;

        point(){
            cout<<"默认构造函数调用"<<endl;
        }

        point(int a,int b){
            x=a;
            y=b;
            cout<<"有参函数构造调用"<<endl;
        }

        point(const point &p){
            x=p.x;
            y=p.y;
            cout<<"拷贝构造函数调用"<<endl;
        }
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01(){

    point p1(1,2);

    point p2(p1);

}

//2. 值传递的方式给函数参数传值
void make(point &p){
    p.x=1;
    p.y=2;
}

void test02(){
    point p3;
    make(p3);
    cout<<p3.x<<" "<<p3.y;
}

//3. 以值方式返回局部对象
int show_x(point p){
    return p.x;
}

void test03(){
    point p4(2,3);
    cout<<show_x(p4);

}

int main(){

    test01();

    test02();

    test03();

    return 0;

}

4.2.4 构造函数调用规则

默认情况下,c 编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c 不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c 不会再提供其他构造函数

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(){
            cout<<"无参构造函数"<<endl;
        }

        point(int a,int b){
            x=a;
            y=b;
            cout<<"有参构造函数"<<endl;
        }

        point(const point &p){
            x=p.x;
            y=p.y;
            cout<<"拷贝构造函数"<<endl;
        }

        ~point(){
            cout<<"析构函数"<<endl;
        }
};

void test01(){

    point p1(1,2);

    //如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
    point p2(p1);

    printf("p2 = (%d,%d)n",p2.x,p2.y);

}

void test02(){
    //如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
    point p3;  //此时如果用户自己没有提供默认构造,会出错

    point p4(3,4);  //用户提供的有参

    point p5(p4);  //此时如果用户没有提供拷贝构造,编译器会提供

    //如果用户提供拷贝构造,编译器不会提供其他构造函数
    point p6;  //此时如果用户自己没有提供默认构造,会出错

    point p7(5,6);  //此时如果用户自己没有提供有参,会出错
    point p8(p7);  //用户自己提供拷贝构造

}

int main(){

    test01();

    test02();

    return 0;

}

4.2.5 深拷贝与浅拷贝

  • 浅拷贝:简单的赋值拷贝操作
  • 深拷贝:在堆区重新申请空间,进行拷贝操作

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;
        int *z;

        point(int a,int b,int h){
            x=a;
            y=b;
            z=new int(h);
        }

        //用户未提供拷贝构造函数,执行浅拷贝

        ~point(){
            if(z!=NULL){
                delete z;
                z=NULL;
            }
        }

};

void test01(){

    point p1(1,2,3);

    point p2(p1);  //用户未提供拷贝构造函数,执行浅拷贝

}

int main(){

    test01();

    return 0;
}

/*程序会崩掉,原因是在用户没有提供拷贝构造函数的前提下,
调用拷贝构造函数是编译器提供的默认拷贝构造函数,对h的地址进行拷贝,实现的是浅拷贝
在执行析构函数时,会造成h的内存重复释放的非法操作

解决方案

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;
        int *z;

        point(int a,int b,int h){
            x=a;
            y=b;
            z=new int(h);
        }

        point(const point &p){
            x=p.x;
            y=p.y;
//          z=p.z;  //浅拷贝执行的操作 
            z=new int(*p.z);  //深拷贝执行的操作
        } 

        ~point(){
            if(z!=NULL){
                delete z;
                z=NULL;
            }
        }

};

void test01(){

    point p1(1,2,3);

    point p2(p1);  //用户提供了拷贝函数,执行深拷贝

}

int main(){

    test01();

    return 0;
}

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题


4.2.6 初始化列表

作用:C 提供了初始化列表语法,用来初始化属性

语法:构造函数():属性1(值1),属性2(值2)... {}

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y,z;

        //传统方式初始化
//      point(int a,int b,int h){
//          x=a;
//          y=b;
//          z=h;
//      }

        //初始化列表方式初始化
        point(int a,int b,int h):x(a),y(b),z(h) {}

};

void test01(){

    point p1(1,2,3);

    cout<<p1.x<<" "<<p1.y<<" "<<p1.z<<endl;  

}

int main(){

    test01();

    return 0;
}

4.2.7 类对象作为类成员

C 类中的成员可以是另一个类的对象,我们称该成员为 对象成员

例如

代码语言:javascript复制
class A{

}

class B{
    A a;
}

B类中有对象A作为成员,A为对象成员当创建B对象时,A与B的构造和析构的顺序

示例:

代码语言:javascript复制
//构造的顺序是 :先调用对象成员的构造,再调用本类构造
//析构顺序与构造相反
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class A{
    public:

    A(){
        cout<<"A的构造函数调用"<<endl;
    }

    ~A(){
        cout<<"A的析构函数调用"<<endl;
    }
};

class B{
    public:
        A a;

        B(){
            cout<<"B的构造函数调用"<<endl;
        }

        ~B(){
            cout<<"B的析构函数调用"<<endl;
        }
};

int main(){

    B b2;

    return 0;
}

4.2.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

示例1 :静态成员变量

代码语言:javascript复制
//静态成员变量特点:
    //1 在编译阶段分配内存
    //2 类内声明,类外初始化
    //3 所有对象共享同一份数据
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        static int x;

    private:
        static int y;  //静态成员变量也是有访问权限的
};

int point::x=10;

int point::y=20;

int main(){

    point p1;

    p1.x=20;  

    cout<<p1.x<<endl;  //1、通过对象访问 

    point p2;

    cout<<p2.x<<endl;  //共享同一份数据

    cout<<point::x<<endl;  //2、通过类名访问 

    //cout<<p2.y<<enl;  //私有权限访问不到

    return 0;

}

示例2:静态成员函数

代码语言:javascript复制
//静态成员函数特点:
    //1 程序共享一个函数
    //2 静态成员函数只能访问静态成员变量
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        static int x;
        int y;

        static void show_pub(){
            cout<<x<<endl;
            //cout<<y<<endl;  //错误,不可以访问非静态成员变量
        }

    private:
        static int z;

        //静态成员函数也是有访问权限的
        static void show_pri(){
            cout<<z<<endl;
        }
};

int point::x=10;
int point::z=30;

int main(){

    point p1;

    p1.y=20;

    //1、通过对象
    p1.show_pub();

    //2、通过类名
    point::show_pub();

    //p1.show_pri();  //私有权限访问不到

    return 0;

} 

4.3 C 对象模型和this指针


4.3.1 成员变量和成员函数分开存储

C 中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point1{

    int x;  //非静态成员变量占对象空间

};

class point2{

    int x;  //非静态成员变量占对象空间

    static int y;  //静态成员变量不占对象空间

    void fx(){  //函数也不占对象空间,所有函数共享一个函数实例

    }

    static void fy(){  //静态成员函数也不占对象空间

    }
};

int main(){

    cout<<sizeof(point1)<<endl;

    cout<<sizeof(point2)<<endl;

}

注意C 编译器会给空对象分配一个字节,用于区分其存储空间


4.3.2 this指针概念

C 中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:这一块代码是如何区分那个对象调用自己的呢?

C 通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

概念

  • this指针是隐含每一个非静态成员函数内的一种指针
  • this指针不需要定义,直接使用即可

用途

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{

    public:

        int x,y;

        //1、当形参和成员变量同名时,可用this指针来区分
        point(int x,int y){
            this->x=x;
            this->y=y;
        }

        point& add(point p){
            this->x =p.x;
            this->y =p.y;
            //返回对象本身
            return *this;
        }

};

void test01(){

    point p1(1,2);

    cout<<p1.x<<" "<<p1.y<<endl;

}

void test02(){

    point p2(1,1);

    point p3(0,0);

    p3.add(p2).add(p2).add(p2);

    cout<<p2.x<<" "<<p2.y<<endl; 

}

int main(){

    test01();

    test02();

    return 0;

}

4.3.3 空指针访问成员函数

C 中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针

如果用到this指针,需要加以判断保证代码的健壮性

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(int a,int b):x(a),y(b) {}

        void show_1(){
            cout<<"YES"<<endl;
        }

        void show_2(){

            cout<<x<<" "<<y<<endl;  //默认使用了this指针 
//          cout<<this->x<<this->y<<endl;  //与上一行等价 

        }
};

int main(){

    point *p1=NULL;

    p1->show_1();   //空指针,可以调用成员函数

    p1->show_2();  //但是如果成员函数中用到了this指针,就不可以了

    return 0;

}

4.3.4 const修饰成员函数

常函数:

  • 成员函数后加const后我们称为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x;
        mutable int y;  //可修改 可变的

        point(){
            x=10;
            y=10;
        }

        //this指针的本质是一个指针常量,指针的指向不可修改
        //如果想让指针指向的值也不可以修改,需要声明常函数

        void show_1() const{
            //const Type* const pointer;
            //this = NULL; //不能修改指针的指向 Person* const this;
            //this->mA = 100; //但是this指针指向的对象的数据是可以修改的

            //const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量

            this->y=20;
            cout<<x<<" "<<y<<endl;
        }

        void show_2(){
            cout<<x<<" "<<y<<endl;
        }

//      void show_no() const{
//          this->x=20;
//      }
};

int main(){

    point p1;

    p1.show_1();  //非常对象可以调用const函数 

    const point p2;   //常量对象  

//  p2.x=20;  //常对象不能修改成员变量的值,但是可以访问
    p2.y=100;  //但是常对象可以修改mutable修饰成员变量

    //常对象访问成员函数
    p2.show_1();

//  p2.show_2();  //常对象只能调用const函数

    return 0;

}

4.4 友元


生活中你的家有客厅(Public),有你的卧室(Private)

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

但是呢,你也可以允许你的好闺蜜好基友进去。

在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

  • 友元的目的就是让一个函数或者类 访问另一个类中私有成员
  • 友元的关键字为 :friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

4.4.1 全局函数做友元

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{

    friend void visit(point &p);  //声明友元函数 

    public:
        int x;

        point(){
            x=10;
            y=10;
        }

    private:
        int y;

};

void visit(point &p){
    cout<<p.x<<endl<<p.y<<endl;
}

int main(){

    point p1;

    visit(p1);

}

4.4.2 类做友元

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{

    friend class show; //声明友元类

    public:
        int x;

        point(){
            x=10;
            y=10;
        }

    private:
        int y;

};

class show{

    public:

        point p1;

        void visit(){
            cout<<p1.x<<" "<<p1.y<<endl;  //可以访问类point里的私有y
        }

};

int main(){

    show s1;

    s1.visit();

    return 0;

}

4.4.3 成员函数做友元

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point;
class show
{
    public:
        show();
        void visit(); //让visit函数作为point的好朋友,可以发访问point中私有内容

    private:
        point *p;
};

class point
{
    //告诉编译器  show类中的visit成员函数 是point好朋友,可以访问私有内容
    friend void show::visit();

    public:
        point();

    public:
        int x;
    private:
        int y;
};

point::point()
{
    this->x=10;
    this->y=10;
}

show::show()
{
    p = new point;
}

void show::visit()
{
    cout << "好基友正在访问" << p->x << endl;
    cout << "好基友正在访问" << p->y << endl;
}

void test01()
{
    show s;
    s.visit();
}

int main(){

    test01();

    return 0;
}

4.5 运算符重载


运算符重载概念:利用operator对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

本质

  • 提供一个operator 运算符()函数,使得A operator 运算符(B)的形式可以化简为A 运算符 B的形式

4.5.1 加号/减号运算符重载

作用:实现两个自定义数据类型相加的运算

示例1

代码语言:javascript复制
//成员函数实现   / -号运算符重载
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(){}

        point(int a,int b):x(a),y(b) {}

        point operator (const point p){  //成员函数实现   号运算符重载
            point t;
            t.x=this->x p.x;
            t.y=this->y p.y;
            return t;
        }

        point operator-(const point p){  //成员函数实现 - 号运算符重载
            point t;
            t.x=this->x-p.x;
            t.y=this->y-p.y;
            return t;
        }

};

int main(){

    point p1(1,1);

    point p2(2,2);

    point p3=p2 p1;

    cout<<p3.x<<" "<<p3.y<<endl;

    p3=p2-p1;

    cout<<p3.x<<" "<<p3.y<<endl;

    return 0;

}

示例2

代码语言:javascript复制
//全局函数实现   / -号运算符重载
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(){}

        point(int a,int b):x(a),y(b){}

};

point operator (const point &p1,const point &p2){  //全局函数实现   号运算符重载
    point t;
    t.x=p1.x p2.x;
    t.y=p1.y p2.y;
    return t;
}

point operator-(const point &p1,const point &p2){  //全局函数实现 - 号运算符重载
    point t;
    t.x=p1.x-p2.x;
    t.y=p1.y-p2.y;
    return t;
}

int main(){

    point p1(1,1);

    point p2(2,2);

    point p3=p1 p2;

    cout<<p3.x<<" "<<p3.y<<endl;

    p3=p2-p1;

    cout<<p3.x<<" "<<p3.y<<endl;

    return 0;

}

总结

  • 对于内置的数据类型的表达式的的运算符是不可能改变的
  • 不要滥用运算符重载

4.5.2 左移运算符重载

作用:可以输出自定义数据类型

注意 :一般使用全局函数实现

示例

代码语言:javascript复制
//全局函数实现左移重载
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(){}

        point(int a,int b):x(a),y(b){}

};

//ostream对象只能有一个
ostream& operator<<(ostream &cout,point &p){
    cout<<p.x<<" "<<p.y<<endl;
    return cout; 
}

int main(){
    point p1(1,1);

    cout<<p1<<"链式输出"<<endl;

    return 0;
}

4.5.3 递增运算符重载

作用: 通过重载递增运算符,实现自己的整型数据

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(){}

        point(int a,int b):x(a),y(b){}

        //前置递增 
        point& operator  (){
            this->x  ;
            this->y  ;
            return *this;
        }

        //后置递增 
        point& operator  (int){  //int用于占位 
            this->x  ;
            this->y  ;
            return *this;
        }
};

int main(){

    point p1(1,1);

    p1  ;

      p1;

    cout<<p1.x<<" "<<p1.y<<endl;

    return 0;

}

4.5.4 赋值运算符重载

C 编译器至少给一个类添加4个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=, 对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int *x,*y;  //开辟到堆区 

        point(){}

        point(int a,int b){
            x=new int(a);  //将数据开辟到堆区
            y=new int(b);
        }

        //重载赋值运算符 
        point& operator=(point &p){
            if(this->x!=NULL){
                delete this->x;
                this->x=NULL;
            }
            if(this->y!=NULL){
                delete this->y;
                this->y=NULL;
            }

            //this->x=p.x;  //编译器提供的代码是浅拷贝 

            this->x=new int(*p.x);  //提供深拷贝 解决浅拷贝的问题
            this->y=new int(*p.y);

            return *this;
        }

        ~point(){
            if(this->x!=NULL){
                delete this->x;
                this->x=NULL;
            }
            if(this->y!=NULL){
                delete this->y;
                this->y=NULL;
            }
        }
};

int main(){

    point p1(1,1);

    point p2(2,2);

    p1=p2;

    cout<<*p1.x<<" "<<*p1.y<<endl;

    return 0;

}

4.5.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(int a,int b):x(a),y(b){}

        bool operator==(const point &p){
            if(this->x==p.x&&this->y==p.y) return 1;
            else return 0;
        }
};

int main(){

    point p1(1,1);

    point p2(2,2);

    if(p1==p2) cout<<"YES"<<endl;
    else cout<<"NO"<<endl;

    return 0; 

}

4.5.6 函数调用运算符重载

特点

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class print{
    public:
        void operator()(auto s){  //重载的()操作符 也称为仿函数
            cout<<s<<endl;
        }
};

class add{
    public:
        int operator()(int a,int b){
            return a b; 
        }
};

void test01(){

    print p;

    p("lys is dog");

}

void test02(){

    add a;

    cout<<a(1,2)<<endl;

}

int main(){

    test01();

    test02();

    return 0;

}

4.6 继承


继承是面向对象三大特性之一


我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码

定义和概念

继承是类的重要特性。A类继承B类,我们称B类为“基类”,A为“派生类”。A类继承了B类之后,A类就具有了B类的部分成员,具体得到了那些成员,这得由两个方面决定:

  • 继承方式
  • 基类成员访问权限

4.6.1 继承的基本语法

基本语法class A : public B

  • A 类称为派生类 或 派生类
  • B 类称为基类 或 基类

示例:对于一个人来说,有姓名,年龄,性别,这些基本特征,而像是职位之类的特征则是因人而异的特征,在创建人的类的时候,我们可以通过继承的技术,减少对基本特征的定义等操作的代码。

普通实现:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

//学生类 
class student{
    public:

        string name,sex;
        int year;

        student(string n,int x,string s):name(n),year(x),sex(s){}

        void show_name(){
            cout<<"名字:"<<name<<endl;
        }

        void show_year(){
            cout<<"年龄:"<<year<<endl;
        }

        void show_sex(){
            if(sex=="boy"){
                cout<<"性别:男"<<endl;
            }
            else cout<<"性别:女"<<endl;
        }

        void show_position(){
            cout<<"是一个学生"<<endl; 
        }

};

//家长类 
class parent{
    public:

        string name,sex;
        int year;

        parent(string n,int x,string s):name(n),year(x),sex(s){}

        void show_name(){
            cout<<"名字:"<<name<<endl;
        }

        void show_year(){
            cout<<"年龄:"<<year<<endl;
        }

        void show_sex(){
            if(sex=="boy"){
                cout<<"性别:男"<<endl;
            }
            else cout<<"性别:女"<<endl;
        }

        void show_position(){
            cout<<"是一名家长"<<endl;
        }
};

//教师类 
class teacher{
    public:

        string name,sex;
        int year;

        teacher(string n,int x,string s):name(n),year(x),sex(s){}

        void show_name(){
            cout<<"名字:"<<name<<endl;
        }

        void show_year(){
            cout<<"年龄:"<<year<<endl;
        }

        void show_sex(){
            if(sex=="boy"){
                cout<<"性别:男"<<endl;
            }
            else cout<<"性别:女"<<endl;
        }

        void show_position(){
            cout<<"是一位老师"<<endl;
        }
};

int main(){

    //学生对象 
    student s1("lys",20,"boy");
    s1.show_name();
    s1.show_year();
    s1.show_position();
    cout<<endl;

    //家长对象 
    parent p1("mama",40,"girl");
    p1.show_name();
    p1.show_year();
    p1.show_position();
    cout<<endl;

    //教师对象 
    teacher t1("yxc",30,"boy");
    t1.show_name();
    t1.show_year();
    t1.show_position();
    cout<<endl;

    return 0;

}

继承实现:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

//用于继承的类 
class person{
    public:
        string name,sex;
        int year;

        person(string n,int x,string s):name(n),year(x),sex(s){}

        void show_name(){
            cout<<"名字:"<<name<<endl;
        }

        void show_year(){
            cout<<"年龄:"<<year<<endl;
        }

        void show_sex(){
            if(sex=="boy"){
                cout<<"性别:男"<<endl;
            }
            else cout<<"性别:女"<<endl;
        }
};

//学生类 
class student : public person{
    public:

        student(string n,int x,string s):person(n,x,s){}

        void show_position(){
            cout<<"是一个学生"<<endl;  //因人而异的特征
        }

};

//家长类 
class parent : public person{
    public:

        parent(string n,int x,string s):person(n,x,s){}

        void show_position(){
            cout<<"是一名家长"<<endl;
        }
};

//教师类 
class teacher : public person{
    public:

        teacher(string n,int x,string s):person(n,x,s){}

        void show_position(){
            cout<<"是一位老师"<<endl;
        }
};

int main(){

    //学生对象 
    student s1("lys",20,"boy");
    s1.show_name();
    s1.show_year();
    s1.show_position();
    cout<<endl;

    //家长对象 
    parent p1("mama",40,"girl");
    p1.show_name();
    p1.show_year();
    p1.show_position();
    cout<<endl;

    //教师对象 
    teacher t1("yxc",30,"boy");
    t1.show_name();
    t1.show_year();
    t1.show_position();
    cout<<endl;

    return 0;

}

总结:

  • 继承的好处:可以减少重复的代码
  • 派生类中的成员,包含两大部分:
    • 一类是从基类继承过来的(基本特征)
    • 一类是自己增加的成员(因人而异的特征)。

从基类继承过过来的表现其共性,而新增的成员体现了其个性


4.6.2 继承方式

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

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

示例1 公共继承:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x;

    protected:
        int y;

    private:
        int z;
};

//公共继承
class point_Pub:public point{
    public:

        //类内 
        point_Pub(){
            x=10;  //可访问 public权限
            y=20;  //可访问 protected权限
            //z=30;  //不可访问 private权限 
        }

        void show(){
            cout<<"x = "<<x<<endl;
            cout<<"y = "<<y<<endl;
        }
};

int main(){

    point_Pub p1;

    p1.show();

    //类外 
    cout<<"p1.x = "<<p1.x<<endl;  //只能访问到公共权限
    //cout<<"p1.y = "<<p1.y<<endl;
    //cout<<"p1.z = "<<p1.z<<endl; 

    return 0; 

}

示例2 保护继承

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x;

    protected:
        int y;

    private:
        int z;
};

//保护继承
class point_Pro:protected point{
    public:

        //类内 
        point_Pro(){
            x=10;  //可访问原来的 public权限,但此时x已经变成了protected权限 
            y=20;  //可访问 protected权限
            //z=30;  //不可访问 private权限 
        }

        void show(){
            cout<<"x = "<<x<<endl;
            cout<<"y = "<<y<<endl;
        }
};

int main(){

    point_Pro p1;

    p1.show();

    //类外 
    //cout<<"p1.x = "<<p1.x<<endl;  //不可访问,原有的public权限变为了protected权限 
    //cout<<"p1.y = "<<p1.y<<endl;
    //cout<<"p1.z = "<<p1.z<<endl; 

    return 0; 

}

示例3 私有继承

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x;

    protected:
        int y;

    private:
        int z;
};

//私有继承
class point_Pri:private point{
    public:

        //类内 
        point_Pri(){
            x=10;  //可访问原来的 public权限,但此时x已经变成了private权限 
            y=20;  //可访问原来的 protected权限,但此时y已经变成了private权限 
            //z=30;  //不可访问 private权限 
        }

        void show(){
            cout<<"x = "<<x<<endl;
            cout<<"y = "<<y<<endl;
        }
};

int main(){

    point_Pri p1;

    p1.show();

    //类外 
//  cout<<"p1.x = "<<p1.x<<endl;  //不可访问,原有的public权限变为了protected权限 
//  cout<<"p1.y = "<<p1.y<<endl;  //不可访问,原有的protected权限变为了private权限 
//  cout<<"p1.z = "<<p1.z<<endl; 

    return 0; 

}

4.6.3 继承中的对象模型

问题:从基类继承过来的成员,哪些属于派生类对象中?

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{

    public:
        int x;

    protected:
        int y;

    private:
        int z; //私有成员只是被隐藏了,但是还是会继承下去

    static int l;
};

class point_son:public point{
    public:
        int m;
};

int main(){

    cout<<sizeof(point_son)<<endl;  //大小为16
    //说明所有基类的非静态成员全部继承了下来

}

结论: 基类中私有成员也是被派生类继承下去了,只是由编译器给隐藏后访问不到


4.6.4 继承中的对象赋值关系

特点

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。 但是必须是基类的指针是指向派生类对象时才是安全的

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        point(int a,int b):x(a),y(b){}
};

class point_son:public point{
    public:
        int s_x=10;

        point_son(int a,int b):point(a,b){}
};

void test01(){

    point_son s1(1,1);

    point p1=s1;  //派生类对象可以赋值给基类对象 
    cout<<p1.x<<" "<<p1.y<<endl;

    point *p2=&s1;  //派生类对象可以赋值给基类指针 
    cout<<p2->x<<" "<<p2->y<<endl; 

    point &p3=s1;  //派生类对象可以赋值给基类引用 
    cout<<p3.x<<" "<<p3.y<<endl;

}

void test02(){

    point p1(1,1);

    //基类对象不能赋值给派生类对象 
    //point_son s1=p1;  

    //基类的指针可以通过强制类型转换赋值给派生类的指针
    point *p2=&p1;      
    point_son *s2=(point_son*)p2;  //此情况可以转换  

    //派生类的指针不可以指向基类的指针,同引用 
    //point_son *s3=&p1;
    //point_son &s3=p2;

    //派生类的对象所占的存储空间通常要比基类的对象大
    //原因就是派生类除了继承基类的成员之外,还拥有自己的成员
    /*所以基类的指针操作派生类的对象时,
    由于基类指针会向操作基类对象那样操作派生类对象,
    而基类对象所占用的内存空间通常小于派生类对象,
    所以基类指针不会超出派生类对象去操作数据*/

}

int main(){

    test01();

    test02();

    return 0;

}

4.6.5 继承中构造和析构顺序

派生类继承基类后,当创建派生类对象,也会调用基类的构造函数

问题:基类和派生类的构造和析构顺序是谁先谁后?

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{

    public:
        int x;

        point(int a):x(a){
            cout<<"基类的构造函数调用"<<endl;
        }

        ~point(){
            cout<<"基类的析构函数调用"<<endl;      
        }
};

class point_son:public point{
    public:
        point_son(int a):point(a){
            cout<<"派生类的构造函数调用"<<endl;
        }

        ~point_son(){
            cout<<"派生类的析构函数调用"<<endl;
        }
};

int main(){

    point_son p1(10);
    //继承中 先调用基类构造函数,再调用派生类构造函数,析构顺序与构造相反

    return 0;

}

总结:继承中 先调用基类构造函数,再调用派生类构造函数,析构顺序与构造相反


4.6.6 继承同名成员处理方式

问题:当派生类与基类出现同名的成员,如何通过派生类对象,访问到派生类或基类中同名的数据呢?

  • 访问派生类同名成员 直接访问即可
  • 访问基类同名成员 需要加作用域

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{

    public:
        int x=20;

        void show(){
            cout<<"基类void show()的函数调用"<<endl;
        }

};

class point_son:public point{
    public:
        //当派生类与基类拥有同名的成员变量,派生类会隐藏基类中所有同名的成员变量 
        int x=10; 

        //当派生类与基类拥有同名的成员函数,派生类会隐藏基类中所有版本的同名成员函数
        void show(){
            cout<<"子类void show()的函数调用"<<endl;
        }
};

int main(){

    point_son s1;

    cout<<"子类point_son下的x = "<<s1.x<<endl;   
    cout<<"基类point下的x = "<<s1.point::x<<endl;  //如果想访问基类中被隐藏的同名成员变量,需要加基类的作用域

    s1.show();
    s1.point::show();  //如果想访问基类中被隐藏的同名成员函数,需要加基类的作用域

    return 0;

}

总结

  1. 派生类对象可以直接访问到派生类中同名成员
  2. 派生类对象加作用域可以访问到基类同名成员
  3. 当派生类与基类拥有同名的成员函数,派生类会隐藏基类中同名成员函数,加作用域可以访问到基类中同名函数

4.6.7 继承同名静态成员处理方式

问题:继承中同名的静态成员在派生类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问派生类同名成员 直接访问即可
  • 访问基类同名成员 需要加作用域

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        static int x;
        static void show(){
            cout<<"基类静态成员函数的调用"<<endl; 
        }
};

class point_son:public point{
    public:
        static int x;
        static void show(){
            cout<<"派生类静态成员函数的调用"<<endl; 
        }   
};

int point::x=20;
int point_son::x=10;

//通过对象访问
void test01(){

    cout<<"通过对象访问"<<endl;

    point_son p1;

    cout<<"子类point_son下的x = "<<p1.x<<endl;
    cout<<"基类point下的x = "<<p1.point::x<<endl;

    p1.show();
    p1.point_son::show();

}

//通过类名访问
void test02(){

    cout<<"通过类名访问"<<endl;

    cout<<"子类point_son下的x = "<<point::x<<endl;
    cout<<"基类point下的x = "<<point_son::x<<endl;

    point::show();
    point_son::show();
    point_son::point::show();  //出现同名,派生类会隐藏掉基类中所有同名成员函数,需要加作作用域访问

}

int main(){

    test01();

    cout<<endl;

    test02();

    return 0;

}

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)


4.6.8 多继承语法

C 允许一个类继承多个类

语法 class 派生类 :继承方式 基类1 , 继承方式 基类2...

注意:多继承可能会引发基类中有同名成员出现,需要加作用域区分

C 实际开发中不建议用多继承

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point_1{
    public:
        int x=10;

        point_1(){
            cout<<"point_1的构造函数调用"<<endl;
        }

        ~point_1(){
            cout<<"point_1的析构函数调用"<<endl;
        }
};

class point_2{
    public:
        int x=20;

        point_2(){
            cout<<"point_2的构造函数调用"<<endl;
        }

        ~point_2(){
            cout<<"point_2的析构函数调用"<<endl; 
        }
};

//语法:class 派生类:继承方式 基类1 ,继承方式 基类2 
class point_son:public point_1,public point_2{
    public:
        int x=30;

        point_son(){
            cout<<"point_son的构造函数调用"<<endl;
        }

        ~point_son(){
            cout<<"point_son的析构函数调用"<<endl;
        }
};

int main(){

    //多继承容易产生成员同名的情况
    //通过使用类名作用域可以区分调用哪一个基类的成员
    point_son s1;

    cout<<"point_1下的x = "<<s1.point_1::x<<endl;

    cout<<"point_2下的x = "<<s1.point_2::x<<endl;

    cout<<"point_son下的x = "<<s1.x<<endl;

    return 0;

}

总结: 多继承中如果基类中出现了同名情况,派生类使用时候要加作用域


4.6.9 菱形继承

菱形继承概念:

  • 两个派生类继承同一个基类
  • 又有某个类同时继承者两个派生类

这种继承被称为菱形继承,或者钻石继承

典型的菱形继承案例:

  • 先创建一个person类作为基类
  • 再创建两个person的派生类father类和mother
  • 最后创建一个son类同时继承father类和mother

菱形继承问题:

  1. father继承了person的数据,mother同样继承了person的数据,当son使用数据时,就会产生二义性。
  2. son继承自person的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        int year;
        string sex;
};

class father:public person{
    public:
        string name;
};

class mother:public person{
    public:
        string name;
};

class son:public father,public mother{
    public:
        string name;
};

int main(){

    son s1;

    s1.father::sex="男"; 
    s1.father::year=40;
    s1.father::name="baba";

    s1.mother::sex="女"; 
    s1.mother::year=38;
    s1.mother::name="mama";

    //实际需要的数据 
    //s1.son::year=20;
    //s1.son::sex="Dog";
    s1.son::name="lys";

    //s1同时继承了father类和mother类的数据,造成了二义性和资源浪费 

    cout<<"father: "<<s1.father::name<<endl<<"性别: "<<s1.father::sex<<endl<<"年龄: "<<s1.father::year<<endl;
    cout<<endl;

    cout<<"mother: "<<s1.mother::name<<endl<<"性别: "<<s1.mother::sex<<endl<<"年龄: "<<s1.mother::year<<endl;
    cout<<endl;

//  cout<<"son: "<<s1.name<<endl<<"性别: "<<s1.sex<<endl<<"年龄: "<<s1.year<<endl;  //保留了两份数据,产生了二义性
    cout<<"son: "<<s1.name<<endl; 
    cout<<endl;  

    return 0; 

}

解决:以上菱形继承带来的问题可以使用虚继承的技术来解决

关键字virtual

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        int year;
        string sex;
};

//继承前加virtual关键字后,变为虚继承
//此时公共的基类person称为虚基类
class father:virtual public person{
    public:
        string name;
};

class mother:virtual public person{
    public:
        string name;
};

class son:public father,public mother{
    public:
        string name;
};

int main(){

    son s1;

    s1.father::sex="男"; 
    s1.father::year=40;
    s1.father::name="baba";

    s1.mother::sex="女"; 
    s1.mother::year=38;
    s1.mother::name="mama";

    //实际需要的数据 
    s1.son::year=20;
    s1.son::sex="Dog";
    s1.son::name="lys";
    //s1现在只保留最后初始化的一份数据 

    cout<<"father: "<<s1.father::name<<endl<<"性别: "<<s1.father::sex<<endl<<"年龄: "<<s1.father::year<<endl;
    cout<<endl;

    cout<<"mother: "<<s1.mother::name<<endl<<"性别: "<<s1.mother::sex<<endl<<"年龄: "<<s1.mother::year<<endl;
    cout<<endl;

    cout<<"son: "<<s1.name<<endl<<"性别: "<<s1.sex<<endl<<"年龄: "<<s1.year<<endl;
    cout<<endl; 

    return 0; 

}

总结

  • 菱形继承带来的主要问题是派生类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

4.7 多态


4.7.1 多态的基本概念

多态是C 面向对象三大特性之一

多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

静态多态和动态多态区别

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

多态满足条件

  • 有继承关系
  • 派生类重写基类中的虚函数

多态使用条件

  • 基类指针或引用指向派生类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        void show(){
            cout<<"是一个人"<<endl;
        }
};

class male:public person{
    public:
        void show(){
            cout<<"是一个男人"<<endl;
        }
};

class female:public person{
    public:
        void show(){
            cout<<"是一个女人"<<endl;
        }
};

//静态多态的函数地址早绑定  编译阶段已经确定了函数地址
void show_sex(person &p){

    p.show();  //调用person的show()函数 

}

int main(){

    male m1;

    show_sex(m1);  //本意是想根据对象的不同调用相应的show()函数

    female f1;

    show_sex(f1);

    return 0;

}

虚函数实现

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了
        virtual void show(){
            cout<<"是一个人"<<endl;
        }
};

class male:public person{
    public:
        virtual void show(){  //重写的函数virtual可加可不加 
            cout<<"是一个男人"<<endl;
        }
};

class female:public person{
    public:
        void show(){
            cout<<"是一个女人"<<endl;
        }
};

//动态多态的函数地址晚绑定  运行阶段才会确定函数地址
void show_sex(person &p){

    p.show();  //调用对应的show()函数 

}

int main(){
    //调用传入对象的函数
    //如果函数地址在编译阶段就能确定,那么静态联编
    //如果函数地址在运行阶段才能确定,就是动态联编

    male m1;

    show_sex(m1);

    female f1;

    show_sex(f1);

    return 0;

}

多态的优点

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

示例

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class base{
    public:
        int x,y;

        virtual int calculate(){
            return 0;
        }
};

//加法计算器
class add:public base{
    public:
        int calculate(){
            return x y;
        }
};

//减法计算器
class sub:public base{
    public:
        int calculate(){
            return x-y;
        }
};

//乘法计算器
class mul:public base{
    public:
        int calculate(){
            return x*y;
        }
};

int main(){

    //基类指针指向派生类对象的加法计算器
    add a;
    base *b1=&a;
    b1->x=10;
    b1->y=20;
    cout<<b1->calculate()<<endl;

    //基类引用指向派生类对象的减法计算器
    sub s;
    base &b2=s;
    b2.x=10;
    b2.y=20;
    cout<<b2.calculate()<<endl;

    //堆区开辟基类指针指向派生类的乘法计算器
    base *b3=new mul;
    b3->x=10;
    b3->y=20;
    cout<<b3->calculate()<<endl;
    delete b3;

    return 0;

}

总结C 开发提倡利用多态设计程序架构,因为多态优点很多


4.7.2 纯虚函数和抽象类

在多态中,通常基类中虚函数的实现是毫无意义的,主要都是调用派生类重写的内容,可以将虚函数改为纯虚函数

纯虚函数语法virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为抽象类(只要有一个函数是纯虚函数,就是抽象类)

抽象类特点

  • 无法实例化对象
  • 派生类必须重写抽象类中的纯虚函数,否则也属于抽象类

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class point{
    public:
        int x,y;

        //纯虚函数
        //类中只要有一个纯虚函数就称为抽象类
        virtual void show()=0;
};

class point_son_1:public point{
    public:

};

class point_son_2:public point{
    public:
        void show(){
            cout<<x<<" "<<y<<endl;
        }
};

void test01(){

//  point p1;  //抽象类 不能实例化对象
//  point_son_1 s1; //没有重写基类里的纯虚函数,仍被视为抽象类 

}

void test02(){

    point_son_2 s2; 
    s2.x=10;
    s2.y=20;
    s2.show();

    point &p2=s2;   
    p2.show();

    point *p3=new point_son_2;
    p3->x=10;
    p3->y=20;
    p3->show();

} 

int main(){

    test01();

    test02();

    return 0;

}

4.7.3 虚析构和纯虚析构

多态使用时,如果派生类中有属性开辟到堆区,那么基类指针释放时无法调用到派生类的析构代码

解决方式:将基类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决基类指针释放派生类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        person(){
            cout<<"person的构造函数调用"<<endl;
        }

        virtual void show()=0;

        ~person(){
            cout<<"person的析构函数调用"<<endl;
        }
};

class student:public person{
    public:
        string *name;

        student(string s){
            cout<<"student的构造函数调用"<<endl;
            name=new string(s);
        }

        void show(){
            cout<<*name<<" is dog "<<endl; 
        }

        ~student(){
            cout<<"student的析构函数调用"<<endl;
            if(name!=NULL){
                delete name;
                name=NULL;
            }
        }
};

int main(){

    person *p=new student("lys");
    p->show();

    //通过基类指针去释放,会导致派生类对象可能清理不干净,造成内存泄漏
    delete p;

    return 0;

}

解决方法1 将基类函数的析构函数改为虚析构

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        person(){
            cout<<"person的构造函数调用"<<endl;
        }

        virtual void show()=0;

        //利用虚析构函数解决基类释放派生类对象时不彻底的问题
        virtual ~person(){
            cout<<"person的虚析构函数调用"<<endl;
        }
};

class student:public person{
    public:
        string *name;

        student(string s){
            cout<<"student的构造函数调用"<<endl;
            name=new string(s);
        }

        void show(){
            cout<<*name<<" is dog "<<endl; 
        }

        ~student(){
            cout<<"student的析构函数调用"<<endl;
            if(name!=NULL){
                delete name;
                name=NULL;
            }
        }
};

int main(){

    person *p=new student("lys");
    p->show();

    delete p;

    return 0;

}

解决方法2 利用纯虚析构函数的方法

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

class person{
    public:
        person(){
            cout<<"person的构造函数调用"<<endl;
        }

        virtual void show()=0;

        //纯虚析构函数 
        virtual ~person()=0;
};

//纯虚析构函数需要实现 可以将基类中开辟的数据释放 
person::~person(){
    cout<<"person的纯虚析构函数调用"<<endl;
}

class student:public person{
    public:
        string *name;

        student(string s){
            cout<<"student的构造函数调用"<<endl;
            name=new string(s);
        }

        void show(){
            cout<<*name<<" is dog "<<endl; 
        }

        ~student(){
            cout<<"student的析构函数调用"<<endl;
            if(name!=NULL){
                delete name;
                name=NULL;
            }
        }
};

int main(){

    person *p=new student("lys");
    p->show();

    delete p;

    return 0;

}

总结

​ 1. 虚析构或纯虚析构就是用来解决通过基类指针释放派生类对象

​ 2. 如果派生类中没有堆区数据,可以不写为虚析构或纯虚析构

​ 3. 拥有纯虚析构函数的类也属于抽象类


5 文件操作


程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C 中对文件操作需要包含头文件 <fstream>

文件类型分为两种:

  1. 文本文件 - 文件以文本的ASCII码形式存储在计算机中
  2. 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

5.1文本文件


5.1.1写文件

步骤:

  1. 包含头文件 #include <fstream>
  2. 创建流对象 ofstream ofs;
  3. 打开文件 ofs.open("文件路径",打开方式);
  4. 写数据 ofs << "写入的数据";
  5. 关闭文件 ofs.close();

文件打开方式:

打开方式

解释

ios::in

为读文件而打开文件

ios::out

为写文件而打开文件

ios::ate

初始位置:文件尾

ios::app

追加方式写文件

ios::trunc

如果文件存在先删除,再创建

ios::binary

二进制方式

注意: 文件打开方式可以配合使用,利用|操作符

例如:用二进制方式写文件 ios::binary | ios:: out

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
#include <fstream>  //包含头文件   
using namespace std;

int main(){

    ofstream o1;  //创建流对象 

    o1.open("test.txt",ios::out);  //打开文件

    //写数据
    o1<<"lys"<<endl;
    o1<<"ege 20"<<endl;
    o1<<"is a dog"<<endl;

    o1.close();  //关闭文件

    return 0;

}

总结

  • 文件操作必须包含头文件 fstream
  • 读文件可以利用 ofstream ,或者fstream
  • 打开文件时候需要指定操作文件的路径,以及打开方式
  • 利用<<可以向文件中写数据
  • 操作完毕,要关闭文件

5.1.2读文件

读文件步骤如下:

  1. 包含头文件 #include <fstream>
  2. 创建流对象 ifstream ifs;
  3. 打开文件并判断文件是否打开成功 ifs.open("文件路径",打开方式);
  4. 读数据 四种方式读取
  5. 关闭文件 ifs.close();

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
#include <fstream>  //包含头文件   
using namespace std;

int main(){

    ifstream i1;  //创建流对象 

    i1.open("test.txt",ios::in);  //打开文件

    if(!i1.is_open()){  //判断文件是否打开成功
        cout<<"找不到该文件"<<endl;
    }

    //读数据

//  //1
//  char s1[1024]={0};
//  while(i1>>s1){
//      cout<<s1<<endl;  //遇到空格读出一次 
//  } 
//
//  //2
//  char s2[1024]={0};
//  while(i1.getline(s2,sizeof(s2))){
//      cout<<s2<<endl;  //遇到换行读出一次 
//  }
//
//  //3
//  char s3;
//  while((s3=i1.get())!=EOF){
//      cout<<s3;  //一个字符读出一次 
//  }

    //4
    string s4;
    while(getline(i1,s4)){
        cout<<s4<<endl;  //遇到换行读出一次 
    } 

    i1.close();  //关闭文件

    return 0;

}

总结

  • 读文件可以利用 ifstream ,或者fstream
  • 利用is_open函数可以判断文件是否打开成功
  • close 关闭文件

5.2 二进制文件


以二进制的方式对文件进行读写操作

打开方式要指定为ios::binary


5.2.1 写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型ostream& write(const char * buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

示例:

代码语言:javascript复制
#include <stdio.h> 
#include <iostream>
#include <string>
#include <algorithm>
#include <fstream>  //包含头文件   
using namespace std;

int main(){

    ofstream o1;  //创建流对象 

    o1.open("test_01.txt",ios::out|ios::binary);  //打开文件

    //写数据
    char s[1024]="lys is a dog";  //string写入,在读出时会出问题
    o1.write((const char*)&s,sizeof(s));

    o1.close();  //关闭文件

    return 0;

}

总结

  • 文件输出流对象 可以通过write函数,以二进制方式写数据
  • 不要用读入string类型
  • 原因stringstl中其实是一个类,这样写入的其实是test_01这个类对象,因此写到文件的其实是这个类的数据和指向这个类的指针。同时,因为string类的字符串是用new在堆上分配的,string类本身只包含字符串的指针,用c_str()这个成员函数可以获得这个指针

5.2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型istream& read(char *buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

示例:

代码语言:javascript复制
#include <iostream>
#include <string>
#include <algorithm>
#include <fstream>  //包含头文件   
using namespace std;

int main(){

    ifstream i1;  //创建流对象 

    i1.open("test_01.txt",ios::out|ios::binary);  //打开文件

    if(!i1.is_open()){  //判断文件是否打开成功
        cout<<"找不到该文件"<<endl;
    }

    //读数据
    char s[1024];
    i1.read((char*)&s,sizeof(s));
    cout<<s<<endl; 

    i1.close();  //关闭文件

    return 0;

}

总结

  • 文件输入流对象 可以通过read函数,以二进制方式读数据

0 人点赞