当前位置:   article > 正文

C++编程核心_c++核心

c++核心


前言

本阶段主要针对C++面向对象的核心和精髓。


一、内存分区模型

内存分区

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

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

内存分区的意义
不同区域存放的数据,生命周期不同,给我们更大的灵活编程。

new操作符

C++中利用new操作符在堆区申请空间,用delete释放空间(由程序员申请和释放)。

int *func(){
    int *a=new int(10);  //申请空间
    return a;
}

int main(){
    int *p=func();
    cout<<*p<<endl;
    delete p;          //释放数据
    int *arr=new int[10];
    for(int i=0;i<10;++i)
        arr[i]=100+i;
    delete [] arr;    //释放数组
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

二、引用

引用的基本使用

作用:给变量起别名
语法:数据类型 & 别名=原名

int main(){
    int a=10;
    int &b=a;
    cout<<a<<endl;   //10
    cout<<b<<endl;   //10
    b=100;
    cout<<a<<endl;   //100
    cout<<b<<endl;   //100
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

使用引用b的值改变会影响a。
在这里插入图片描述

引用注意事项

  • 引用必须初始化!!!
  • 引用在初始化后,不可以改变(比如c最开始是a的引用,就不能再改为b的引用)!!!
int main(){
    int a=10;
    int b=20;
    // int &c; //定义时不初始化会报错
    int &c=a;
    cout<<a<<endl;  //10
    cout<<b<<endl;  //20
    cout<<c<<endl;  //10
    c=b;  //赋值操作,把b的值赋给c,并不是更改引用哈。
    cout<<a<<endl;  //20
    cout<<b<<endl;  //20 
    cout<<c<<endl;  //20
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

引用做函数参数

//值传递
void swap1(int x,int y){
    int temp=x;
    x=y;
    y=temp;
}

//地址传递
void swap2(int *p1,int *p2){
    int temp=*p1;
    *p1=*p2;
    *p2=temp;
}

//引用传递
void swap3(int &p1,int &p2){
    int temp=p1;
    p1=p2;
    p2=temp;
}

int main() {
    int a=10;
    int b=20;
    swap1(a,b);      //值传递不会改变值
    cout<<a<<endl;   // 10  
    cout<<b<<endl;   // 20
    swap2(&a,&b);    //传递地址会改变值
    cout<<a<<endl;   // 20
    cout<<b<<endl;   // 10
    swap3(a, b);     //引用传递会改变值
    cout<<a<<endl;   // 10
    cout<<b<<endl;   // 20
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

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

引用做函数返回值

注意:引用是可以作为函数的返回值存在的。
不要返回局部变量引用。

//返回局部变量引用
int & test1(){
    int a=10;  //局部变量
    return a;  //会提示: warning: reference to stack memory associated with local variable 'a' returned,局部变量是放在栈区的,代码运行结束后会释放。
}
//返回静态变量引用
int & test2(){
    static int a=20;  //静态变量存放在全局区,全局区上的数据在程序结束之后才由系统释放;
    return a;
}
int main() {
    int & ref=test1();
    cout<<ref<<endl;  //10第一次运行结果正确,因为编译器做了保留。
    cout<<ref<<endl;  //第二次就运行错误,因为a的内存已经释放。
    int &ref2=test2();
    cout<<ref2<<endl; //20
    test2()=1000;
    cout<<ref2<<endl;  //1000

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

所以说引用一般只做为左值。

引用的本质

引用的本质在C++内部实现是一个指针常量。

void func(int &p){
    p=100;   //C+发现p是引用,自动转换为*p=1000;
}

int main() {
    int a=10;
    int &ref=a;
    //C+发现ref是引用,自动转换为int* const ref=&a;指针常量是指针指向不可以改变,但是指向的值可以改变。
    ref=20;      //内部发现ref为引用,自动转换为* ref=20;
    cout<<a<<endl;    //20
    cout<<ref<<endl;  //20
    func(a);
    cout<<a<<endl;    //100
    cout<<ref<<endl;  //100
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

常量引用

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

//引用使用的场景,通常用来修饰形参
void showValue(const int &p){
    // p=20;  p为只读,不可修改。
    //  p+=10;   使用const修饰无法进行+=操作
    cout<<p<<endl;
}
int main() {
    // int & ref=10;   错误,因为引用本身需要一个合法的内存空间,所以错误;
    //可以在前面加入const,编译器优化代码,int temp=10,const int & ref=temp;
    const int & ref=10;  //加入const之后不能修改变量
    cout<<ref<<endl;
    int a=10;
    showValue(a);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

三、函数提高

函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。
语法: 返回值类型 函数名 (参数=默认值){}

1.如果从某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值;
2.如果函数声明有默认值,函数实现的时候就不能有默认参数;

int func1(int a,int b=10,int c=20){
    return a+b+c;
}

int func2(int a=10,int b=20);  //函数声明
int func2(int a,int b){        //函数实现
    return a+b;
}

int main() {
    cout<<func1(10,30,40)<<endl;  //80
    cout<<func1(10)<<endl;        //40
    cout<<func2(30,50)<<endl;     //80
    cout<<func2()<<endl;          //30

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

函数占位参数

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

//函数占位参数,占位参数也可以有默认参数
void func(int a,int){
    cout<<a<<endl;
    cout<<"this is a func"<<endl;
}
int main() {
    func(10, 10);
    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

函数重载

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

函数重载满足条件:

  • 同一个作用域下;
  • 函数名称相同;
  • 函数参数类型不同或者个数不同或者顺序不同
  • 注意函数返回值类型不可以作为函数重载条件。
void func(){
    cout<<"func()"<<endl;
}
void func(int a){
    cout<<"func(int a)"<<endl;
}
void func(double a){
    cout<<"func(double a)"<<endl;
}
void func(int a,int b){
    cout<<"func(int a,int b)"<<endl;
}
int main() {
    func();
    func(1);
    func(10.0);
    func(2,3);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

函数重载注意事项:

  • 引用作为重载条件。
  • 函数重载碰到函数默认参数。
//引用作为重载条件
void func(int &a){
    cout<<"func(int &a)"<<endl;
}

void func(const int &a){
    cout<<"func(const int &a)"<<endl;
}

//函数重载碰到函数默认参数
void func2(int a,int b=10){
    cout<<"func2(int a,int b=10)"<<endl;
}

void func2(int a){
    cout<<"func2(int a)"<<endl;
}

int main(){
    int a=10;
    func(a);
    func(10);
    //func2(10);  此时会出现错误,因为碰到默认参数会产生歧义
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

四、类和对象

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

封装

封装是C++面向对象三大特性之一,封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制
    访问权限有三种
    public 公共权限:类内可以访问,类外可以访问
    protected 保护权限:类内可以访问,类外不可以访问
    private 私有权限:类内可以访问,类外不可以访问
class Person{
    public:
      string name;
    protected:
      string car;
    private:
      int password;
    public:
    void func(){
        name="张三";
        car="拖拉机";
        password=123456;
    }
};
int main(){
    Person p;
    p.name="李四";
    // p.car="奔驰";        保护权限类外访问不到
    // p.password=1231234;  私有权限类外访问不到
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

struct与class的唯一区别就在于默认的访问权限不同。

  • struct默认权限为public;
  • class默认权限为private;

对象的初始化和清理

构造函数和析构函数

当使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。C++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制需要做的事情,所以即使我们不提供构造和析构,编译器也会提供相应的函数,不过编译器提供的构造函数和析构函数是空实现

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

构造函数语法:类名(){}
在这里插入图片描述
析构函数语法:~类名(){}
在这里插入图片描述
在这里插入图片描述

构造函数的分类及调用

两种分类方式:
按参数分:有参构造和无参构造
按类型分:普通构造和拷贝构造

三种调用方式
括号法
显示法
隐式转换法

class Person{
    public:
       Person(){
           cout<<"无参构造函数"<<endl;
       };
       Person(int a){
           cout<<"有参构造函数"<<endl;
       };
       Person(const Person& p){
           cout<<"拷贝构造函数"<<endl;
       }
       ~Person(){
           cout<<"析构函数调用"<<endl;
       };
       int age;
};
int main(){
    //括号法
    Person p1(10);
    //显式法
    Person p2=Person(12);
    Person p3=Person(p2);
    //隐式转换法
    Person p4=10;   //相当于 Person p4=Person(10);
    Person p5=p4;   //相等于 Person p5=Person(p4);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

拷贝构造函数时机

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

  • 使用一个已经创建完毕的对象来初始化一个对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
class Person{
    public:
       Person(){
           cout<<"无参构造函数"<<endl;
           mage=0;
       };
       Person(int age){
           cout<<"有参构造函数"<<endl;
           mage=age;
       };
       Person(const Person& p){
           cout<<"拷贝构造函数"<<endl;
           mage=p.mage;
       }
       ~Person(){
           cout<<"析构函数调用"<<endl;
       };
       int mage;
};
//1、使用一个已经创建完毕的对象来初始化一个新对象
void test1(){
    Person man(100);     //有参构造
    Person newman(man);  //拷贝构造
    Person newman2=man;  //拷贝构造
    Person newman3;      //无参构造
    newman3=man;         //不是调用拷贝构造,仅是赋值
}
//2、值传递的方式给函数参数传值
void dowork(Person p1){} //相当于Person p1=p;
void test2(){
    Person p;   //无参构造函数
    dowork(p);
}
//3、以值方式返回局部对象
Person dowork2(){
    Person p1;
    cout<<&p1<<endl;
    return p1;
}
void test3(){
    Person p=dowork2();
    cout<<&p<<endl;
}
int main(){
    //test1();
    //test2();
    test3();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

构造函数调用规则

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

  1. 默认构造函数(无参)
  2. 默认析构函数
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

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

深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作;(编译器为我们提供的拷贝构造函数就是简单的浅拷贝)
深拷贝:在堆区重新申请空间,进行拷贝操作。
浅拷贝Part:

class Person{
    public:
       Person(){
           cout<<"无参构造函数"<<endl;
           mage=0;
       };
       Person(int age){
           cout<<"有参构造函数"<<endl;
           mage=age;
       };
       ~Person(){
           cout<<"析构函数调用"<<endl;
       };
       int mage;
};

int main(){
    Person p1(18);
    cout<<p1.mage<<endl;
    Person p2(p1);    //编译器提供的默认拷贝构造函数,为浅拷贝
    cout<<p2.mage<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

深拷贝Part:

class Person{
    public:
       Person(){
           cout<<"无参构造函数"<<endl;
           mage=0;
       };
       Person(int age,int height){
           cout<<"有参构造函数"<<endl;
           mage=age;
           mheight=new int(height);
       };
       ~Person(){
           cout<<"析构函数调用"<<endl;
           if(mheight!=NULL)   //对堆区申请的数据进行释放
              delete mheight;
       };
       int mage;
       int *mheight;  //需要在申请空间
};

int main(){
    Person p1(18,180);
    cout<<p1.mage<<" "<<*p1.mheight<<endl;
    Person p2(p1);
    cout<<p2.mage<<" "<<*p2.mheight<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在析构函数中对申请的堆区mheight进行释放,会提示:

free(): double free detected in tcache 2
Aborted (core dumped)
所以:如果属性有在堆区开辟的,为防止浅拷贝带来的问题——“堆区内容重复释放”,一定要自己提供拷贝构造函数,如下所示:

class Person{
    public:
       Person(){
           cout<<"无参构造函数"<<endl;
           mage=0;
       };
       Person(int age,int height){
           cout<<"有参构造函数"<<endl;
           mage=age;
           mheight=new int(height);
       };
       Person(const Person& p){
           cout<<"拷贝构造函数"<<endl;
           //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题。
           mage=p.mage;
           mheight=p.mheight;  //浅拷贝操作
           mheight=new int(*p.mheight);  //深拷贝操作
       }
       ~Person(){
           cout<<"析构函数调用"<<endl;
           if(mheight!=NULL)   //对堆区申请的数据进行释放
              delete mheight;
       };
       int mage;
       int *mheight;  //需要在申请空间
};

int main(){
    Person p1(18,180);
    cout<<p1.mage<<" "<<*p1.mheight<<endl;
    Person p2(p1);
    cout<<p2.mage<<" "<<*p2.mheight<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

初始化列表

C++提供了初始化列表语法,用来初始化属性。
语法:构造函数():属性1(值1),属性2(值2),…{}

class Person{
    public:
        //传统方式初始化
    //    Person(int a,int b,int c){
    //        ma=a;
    //        mb=b;
    //        mc=c;
    //    }
       //初始化列表方式初始化
       Person(int a,int b,int c): ma(a),mb(b),mc(c){}
       void printPerson(){
           cout<<ma<<endl;
           cout<<mb<<endl;
           cout<<mc<<endl;
       }
    private:
       int ma;
       int mb;
       int mc;
};
int main(){
    Person p(1,2,3);
    p.printPerson();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

类对象作为类成员

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

class Phone{
    public:
       Phone(string name){
           mname=name;
           cout<<"Phone构造"<<endl;
       }
       ~Phone(){
           cout<<"Phone析构"<<endl;
       }
string mname;
};
class Person{
    public:
        Person(string name1,string name2): pname(name1),phone(name2){
            cout<<"Person构造"<<endl;
        }
        ~Person(){
            cout<<"Person析构"<<endl;
        }
        void playGame(){
            cout<<pname<<"使用"<<phone.mname<<"牌手机"<<endl;
        }
    string pname;
    Phone phone;

};
int main(){
    //Phone构造
    //Person构造
    Person p("张三","三星");
    p.playGame();
    //Person析构
    //Phone析构
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

当类中成员是其他类对象时,我们称该成员为对象成员。
构造的顺序是:先调用对象成员的构造,再调用本类构造。
析构的顺序与构造顺序相反。

静态成员

静态成员分为:

  • 静态成员变量
    1. 所有对象共享一份数据
    2. 在编译阶段分配内存
    3. 类内声明,类外初始化
  • 静态成员函数
    1. 所有对象共享同一个函数
    2. 静态成员函数只能访问静态成员变量
class Person{
    public:
        static int ma;
    private:
        static int mb;
};
int Person::ma=10;
int Person::mb=10;
int main(){
    Person p1;
    p1.ma=100;
    cout<<p1.ma<<endl;  //100
    Person p2;
    p2.ma=200;
    //p2.mb=300; //私有权限访问不到
    cout<<p1.ma<<endl;  //200 共享一份数据所以,p1的ma值也会发生改变;
    cout<<p2.ma<<endl;  //200

    cout<<Person::ma<<endl;
    //cout<<Person::mb<<endl;  //私有权限访问不到
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

C++对象模型和this指针

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

在C++中,类内的成员变量和成员函数分开存储,只有非经验成员变量才属于类的对象。
空对象占内存为1,C++为每个空对象也分配一个内存空间,用于区分对象占内存的位置。

class Person{
    
};

int main(){
    Person p;
    cout<<sizeof(p)<<endl;  //结果是1
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
class Person{
public:
    Person(){
        ma=0;
    }
    //非静态成员变量占对象空间
    int ma;
    //静态成员变量不占对象空间
    static int mb;
    //函数不占对象空间,非静态成员函数只会诞生一份函数实例,也就说多个同类型的对象会公用一份代码。
    void func(){
        cout<<this->ma<<endl;
    }
    //静态成员函数也不占对象空间
    static void funcb(){
    }
};
int main(){
    cout<<sizeof(Person)<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

this指针概念

每个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码。那么问题是:这一块代码是如何区分那个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针,解决上述问题,this指针指向被调用的成员函数所属对象
在这里插入图片描述
this指针是隐含每一个非静态成员函数内的一种指针;
this指针不需要定义,直接使用即可;
指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分;
  • 在类的非静态成员函数中返回对象本身,可使用return *this;
class Person{
public:
    Person(int age){
        //当形参和成员变量同名时,可用this指针来区分,this指针指向调用此函数的对象;
        this->age=age;
    }
    Person & PersonAddPerson(Person p){  //返回的是对象的引用。
        this->age+=p.age;
        //在类的非静态成员函数中返回对象本身,可使用return *this;
        return *this;
    }
    int age;
};
int main(){
    Person p1(10);
    cout<<p1.age<<endl;
    Person p2(10);
    //链式编程思想
    p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
    cout<<p2.age<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有咩有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性。
在这里插入图片描述

const修饰成员函数

常函数:

  • 成员函数后加const后我们成为这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改
    常对象:
  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
    在这里插入图片描述
    在这里插入图片描述

友元

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

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

全局函数做友元

class Building{
    //告诉编译器goodday全局函数是Building类的好朋友,可以访问类中的私有内容
    friend void goodday(Building *building);
    public:
         Building(){
             this->sittingroom="客厅";
             this->bedroom="卧室";
         }
    private:
         string sittingroom;
         string bedroom;
};

void goodday(Building *building){
    cout<<building->sittingroom<<endl;
    cout<<building->bedroom<<endl;
}
int main(){
    Building building;
    goodday(&building);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

类做友元

class goodDay{
}
  • 1
  • 2
class Building{
      //告诉编译器goodDay类是Building类的好朋友,可以访问Building类中私有内容。
      friend class goodDay;
}
  • 1
  • 2
  • 3
  • 4

成员函数做友元

class goodDay{
       goodDay();
       void visit();
}
  • 1
  • 2
  • 3
  • 4
class Building{
      //告诉编译器goodDay类是Building类的好朋友,可以访问Building类中私有内容。
      friend class goodDay::visit();
}
  • 1
  • 2
  • 3
  • 4

运算符重载

对于内置数据类型,编译器是知道如何进行运算的。
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

加号运算符重载

可以使用成员函数做重载Person operator+(Person &p)也可以使用全局函数做重载Person operator+(Person &p1,Person &p2)

class Person{
public:
   int ma;
   int mb;
   //1、成员函数做重载+
//    Person operator+(Person &p){
//        Person temp;
//        temp.ma=this->ma+p.ma;
//        temp.mb=this->mb+p.mb;
//        return temp;
//    }

};
//全局函数做重载+
Person operator+(Person &p1,Person &p2){
    Person temp;
    temp.ma=p1.ma+p2.ma;
    temp.mb=p1.mb+p2.mb;
    return temp;
}

//或者+int类型等
Person operator+(Person &p1,int num){
    Person temp;
    temp.ma=p1.ma+num;
    temp.mb=p1.mb+num;
    return temp;
}

int main(){
    Person p1;
    p1.ma=10;
    p1.mb=20;
    Person p2;
    p2.ma=31;
    p2.mb=11;
    Person p3=p1+p2;
    cout<<p3.ma<<" "<<p3.mb<<endl;  //分别是41和31
    Person p4=p1+40;
    cout<<p4.ma<<" "<<p4.mb<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

成员函数重载本质调用:
Person p3=p1.operator+(p2)
全局函数重载本质调用
Person p3=operator+(p1,p2);

左移运算符重载

class Person{
public:
   int ma=10;
   int mb;
   //利用成员函数返回函数重载符——不可,因为无法实现cout在左侧
};

//利用全局函数做重载
ostream & operator<<(ostream &cout, Person &p){
    cout<<p.ma<<endl;
    cout<<p.mb<<endl;
    return cout;
}

int main(){
   Person p;
   p.ma=100;
   p.mb=100;
   cout<<p<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

递增运算符重载

class myInteger{
public:
  friend ostream & operator<<(ostream &cout,myInteger myint);
  myInteger(){
      num=0;
  }
  //重载前置++
  myInteger & operator++(){  //返回引用是为了只对当前数据进行+操作
      num+=1;
      return *this;
  }
  //重载后置++
  myInteger operator++(int){  //前置递增和后置递增用传入int做区分   且前置递增返回的是值,不是引用,因为temp作为局部变量,使用结束后就会被释放
      //先记录结果     
      myInteger temp=*this;
      //做家操作
      num+=1;
      //返回之前的操作
      return temp;
  }
private:
  int num;
};

ostream & operator<<(ostream &cout,myInteger myint){
    cout<<myint.num;
    return cout;
}

int main(){
    myInteger integer1;
    cout<<++(++integer1)<<endl;   //是2
    cout<<integer1<<endl;         //也是2
    myInteger integer2;
    cout<<integer2++<<endl;       //是0
    cout<<integer2<<endl;         //是1
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

赋值运算符重载

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

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数(对属性进行值拷贝)
  • 赋值运算符operator=,对属性进行值拷贝
class person{
public:
person(int age){
    mage=new int(age);
}
~person(){
    if(mage!=NULL){
         delete mage;
         mage=NULL;
    }
}

person & operator=(person &p){
    //应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
    if(mage!=NULL){
        delete mage;
        mage=NULL;
    }
    //深拷贝
     mage=new int(*p.mage);
     //返回自身
     return *this;
}

int *mage;
};


int main(){
    person p(10);
    person p2(20);
    person p3(30);
    cout<<*p.mage<<endl;
    cout<<*p2.mage<<endl;
    cout<<*p3.mage<<endl;
    p3=p2=p; 
    cout<<*p.mage<<endl;
    cout<<*p2.mage<<endl;
    cout<<*p3.mage<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

关系运算符重载

class Person{
public:
Person(string name,int age){
    mname=name;
    mage=age;
}
bool operator==(Person &p){
    if(this->mname==p.mname && this->mage==p.mage)
      return true;
    return false;
}

string mname;
int mage;
};

int main(){
    Person p1("Tom",18);
    Person p2("Tom",18);
    if(p1==p2){
        cout<<"p1和p2是相等的"<<endl;
    }else {
    cout<<"p1和p2不相等"<<endl;
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

函数调用运算符重载

  • 函数调用运算符()也可以重载
  • 由于重载后使用的方式非常像函数调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
class MyPrint{
public:
void operator()(string text){
    cout<<text<<endl;
}
};

void printtest(string text){
    cout<<text<<endl;
}


class MyAdd{
public:
int operator()(int num1,int num2){
    return num1+num2;
}
};

int main(){
    MyPrint myPrint;
    myPrint("hello world");  //由于使用起来非常类似于函数调用,因此称为仿函数。
    printtest("hello world");
    MyAdd myadd;
    cout<<myadd(10,40)<<endl;  //或者写成下面的样式
    //匿名对象调用
    cout<<MyAdd()(10,40)<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

继承

继承的好处:可以减少重复的代码;
class A:public B;
A类称为子类或者派生类;
B类称为父类或者基类;

派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员;
从基类继承过来的表现其共性,而新增的成员体现了其个性;

继承方式

继承方式一共有三种:

  • 公共继承public
  • 保护继承protected
  • 私有继承private

在这里插入图片描述

class Base{
    public:
      int ma;
    protected:
      int mb;
    private:
      int mc;
}
//公共继承
class Son : public Base{
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述

父类中私有成员也被子类继承下去了,只是由编译器给隐藏后访问不到。

继承中构造和析构顺序

在这里插入图片描述

继承同名成员处理方式

当子类与父类出现同名的成员,如果通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员考虑,需要加作用域
class Base{
public:
      Base(){
          ma=100;
      }
      void func(){
          cout<<"Base中的func函数"<<endl;
      }
      static void func2(){
          cout<<"Base中的静态函数func2"<<endl;
      }
      int ma;
      static int mb;
};
class Son : public Base{
public:
     Son(){
         ma=200;
     }
     void func(){
         cout<<"Son中的func函数"<<endl;
     }
     static void func2(){
         cout<<"Son中的静态函数func2"<<endl;
     }
     int ma;
     static int mb;
};
int main(){
    Son s;
    cout<<s.ma<<endl;        //访问本类(子类)中函数直接访问就可
    cout<<s.Base::ma<<endl;  //想要访问父类中的同名函数,需要加父类作用域
    s.func();
    s.Base::func();
    cout<<Son::mb<<endl;
    cout<<Son::Base::mb<<endl;
    Son::func2();
    Son::Base::func2();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

多继承语法

class Son : public Base1,public Base2{

}
  • 1
  • 2
  • 3

多继承中如果父类中出现了同名情况,子类使用时候要加作用域;

菱形继承

两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被称为菱形继承。
在这里插入图片描述
菱形继承问题:
1、羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性;
2、羊驼继承自动物的数据继承了两份,但这份数据只需要一份就可以了。

利用虚继承可以解决菱形继承问题:

class Animal{
public:
    int mage;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal{};
class Tuo : virtual public Animal{};
class SheepTuo : public Sheep,public Tuo{};

int main(){
    SheepTuo st;
    st.Sheep::mage=100;
    st.Tuo::mage=200;
    cout<<st.Sheep::mage<<endl;
    cout<<st.Tuo::mage<<endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

多态

多态的基本概念

多态分为两类:

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名;
  • 动态多态:派生类和虚函数属于实现运行时多态;
    静态编译与动态编译的区别:
  • 如果函数地址在编译阶段就可以确定,那么就是静态编译
  • 如果函数地址在运行阶段才能确定,那么就是动态编译
class Animal{
public:
    //函数前加virtual关键字,变为虚函数
    virtual void speak(){
        cout<<"animal speak"<<endl;
    }
};
class Dog : public Animal{
public:
    void speak(){
        cout<<"dog speak"<<endl;
    }
};

class Cat : public Animal{
public:
     void speak(){
         cout<<"cat speak"<<endl;
     }
};
void dotalk(Animal &animal){
    animal.speak();
}
int main(){
    Animal animal;
    dotalk(animal);
    Dog dog;
    dotalk(dog);
    Cat cat;
    dotalk(cat);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

多态满足条件:
1.有继承关系
2.子类重写父类中的虚函数
多态使用:
父类指针或引用指向子类对象

纯虚函数和抽象类

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

纯虚函数语法: virtual 返回值类型 函数名 (参数列表)=0;
当类中有了纯虚函数,这个类也称为抽象类抽象类无法实例化对象。子类必须重写父类中的纯虚函数,否则也属于抽象类。

class Base{
public:
    //纯虚函数
    virtual void func()=0;
};

class Son : public Base{
public:
    //重写方法时依旧要加virtual
    virtual void func(){
        cout<<"func调用"<<endl;
    };
};

int main(){
    Base *base=NULL;  //抽象类无法实例化对象
    base=new Son();
    base->func();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。

虚析构和纯虚析构共性

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

虚析构和纯虚析构区别

  • 如果是纯虚析构,该类属于抽象类,无法实现实例化对象。
class Animal{
public:
    Animal(){
        cout<<"Animal构造函数"<<endl;
    }
    virtual void speak()=0;
    virtual ~Animal(){
        cout<<"Animal析构函数"<<endl;
    }
};
class Cat: public Animal{
public:
    Cat(string name){
        catname=new string(name);
    }
    virtual void speak(){
        cout<<*catname<<"小猫在说话"<<endl;
    }
    ~Cat(){
        cout<<"Cat析构函数"<<endl;
        if(this->catname!=NULL){
            delete catname;
            catname=NULL;
        }
    }
    string *catname;
};
int main(){
    Animal *animal=new Cat("Tom");
    animal->speak();
    //通过父类指针去释放子类,可能会导致子类对象清理不干净,造成内存泄露
    //怎么解决?给父类增加一个虚析构函数
    //虚析构函数就是用来解决通过父类指针释放子类对象时,释放不干净的问题!!
    delete animal;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

五、文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化。

文件类型主要分为两种:
1.文本文件——文件以文本的ASII码形式存储在计算机中;
2.二进制文件——文件以文本的二进制形式存储在计算机中,用于一般不能直接读懂它们。
操作文件的三大类:
1.ofstream:写操作
2.ifstream:读操作
3.fstream:读写操作

文本文件

写文件

在这里插入图片描述
文件打开方式:
在这里插入图片描述

读文件

在这里插入图片描述

二进制文件

写文件

class Person{
    public:
        char mname[64];
        char mage;
};
void test(){
    ofstream ofs;
    ofs.open("person.txt",ios::out | ios::binary);
    Person p={"张三",18};
    ofs.write((const char *)&p,sizeof(p));
    ofs.close();
}
int main(){
    test();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/181175
推荐阅读
相关标签
  

闽ICP备14008679号