• 【C++】类和对象的基础概念问题


    类的引入

    我们都说“C++是基于 面向对象(OOP-Object Oriented Programming) 的语言,关注的重点是对象” “单身?new一个对象出来就好了”等等…

    且不谈面向对象,先说对象是什么?

    C语言中用 struct 声明结构体,结构体成员可以是不同类型的变量,例如将一个人的姓名、年龄、性别等属性都声明成结构体的成员变量,那这个结构体就可以表示一个人的基本信息。那如果把一个人的行为封装成函数,例如吃、喝、拉、撒…把这些函数也放在结构体里面,那么这个结构体是不是就可以表示一类人了呢?既包括这类人的基本信息,又包括这类人的基本行为。

    可惜的是,C语言不支持在结构体中声明定义函数。

    而C++则引入了类的概念,既可以在类中声明不同类型的变量,又可以声明定义成员函数,然后用这个类定义一个具体的对象。


    类的定义

    如何定义

    C++是C语言的增强版,自然而然地支持C语言的语法,所以可以用 struct 定义类,但毕竟是一种新语言,总得有点新东西,所以C++更喜欢用 class 来代替。

    class className {
    	// 类体:由成员函数和成员变量组成
    };  // 一定要注意后面的分号
    
    • 1
    • 2
    • 3

    class为定义类的关键字,ClassName为类的名字,{} 中为类的主体。

    类中的元素称为类的成员:类中的数据称为类的属性或者成员变量,类中的函数称为类的方法或者成员函数


    类的两种定义方式

    1. 声明和定义全部放在类体中。由于成员变量只能声明不能定义,所以前面指的定义是指成员函数的定义。需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

      class Person {
      public:
      	void DisplayInfo() {
      		cout << _name << endl;
      		cout << _sex << endl;
      		cout << _age << endl;
      	}
          //...
      
      private:
      	char* _name;
      	char* _sex;
      	int _age;
      	//...
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
    2. 声明放在 .h 文件中,定义放在 .cpp 文件中。这里的声明是成员变量和成员函数的声明,定义是成员函数的定义。

      //person.h
      class Person {
      public:
      	void DisplayInfo();
          //...
          
      private:
      	char* _name;
      	char* _sex;
      	int _age;
      	//...
      }
      
      //person.h
      #include "person.h"
      void Person::DisplayInfo() {
          cout << _name << endl;
          cout << _sex << endl;
          cout << _age << endl;
      }
      //...
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

      注意:函数定义时要用域操作符有一个名为 DisplayInfo 的函数,它们并不冲突。


    类的作用域

    类定义了一个新的作用域,类的所有成员都在类的这个作用域中。

    跟命名空间定义个一个域类似,想在类外面定义访问类的成员需要用 :: 作用域解析符,指名要访问的成员属于哪个类。

    就比如上面第二种定义类的方式中在类外面定义成员函数


    类的访问限定符

    访问限定符

    类有三大访问限定符,如上面用到的 public(公有)private(私有) ,此外还有 protected(保护)

    1. public 修饰的成员可以在类外面可以直接被访问;

    2. protectedprivate 修饰的成员在类外不能直接被访问(此处 protectedprivate 是类似的);

    3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;

    4. class 的默认访问权限为 privatestructpublic (因为 struct 要兼容C)。

    解释一下:

    首先是在类里面和类外面访问的问题。在类里面访问一般是通过成员函数访问,比如前面声明的一个 person 类,成员函数 DisplayInfo 就访问了类的成员变量,这种就属于在类里面访问,而且由于成员变量都是 private 修饰的,也只能在类里面访问。在类外面访问就是出了类再访问,比如我在 main 函数中创建了一个对象,通过这个对象调用 DisplayInfo 函数,这就是在类外面访问,而如果想通过这个对象直接访问私有变量则不行:image-20220811122122696

    class 的默认访问权限是 private ,所以我如果不写访问限定符,那么默认所有定义在类里的成员变量和成员函数都是私有的:

    class Person {
    	void DisplayInfo() {
    		cout << _name << endl;
    		cout << _sex << endl;
    		cout << _age << endl;
    	}
    	//...
    
    	char* _name;
    	char* _sex;
    	int _age;
    	//...
    };
    
    int main() {
    	Person p;
    	p.DisplayInfo();
    	cout << p._name << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220811122535203

    而如果我在开头写了一个 public ,后面声明成员变量的时候忘记用 private 限定访问了,那成员变量也变成共有的了:

    class Person {
    public:
    	void DisplayInfo() {
    		cout << _name << endl;
    		cout << _sex << endl;
    		cout << _age << endl;
    	}
    	//...
    
    	char* _name;
    	char* _sex;
    	int _age;
    	//...
    };
    
    int main() {
    	Person p;
    	p.DisplayInfo();
    	cout << p._name << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这样就不会报错,但是 p 的成员有指针变量,默认构造函数不对它处理,所以这样实际是编不过的,这一点后面会讲。


    初步理解封装

    面向对象有三大特性,封装继承多态,继承和多态这里不谈,下面先理解一下封装

    封装实际上就是将数据和操作数据的方法(也就是函数)进行有机结合,隐藏对象的属性(成员变量)和实现细节(成员函数),仅对外公开接口来和对象交互

    例如我实现了一个火车站类,对于买票这一行为,我只给你一个买票的窗口,你只需要选定车次付款就可以了,具体我是怎么处理你买票的这个过程是不对你公开的,这样仅通过对外公开接口就实现了人和车站的一种交互行为。

    封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但我们目的不是全封装起来,不让别人看。所以我们开放了售票通
    道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。
    不想给别人看到的,我们使用 protected/private 把成员封装起来。开放一些共有的成员函数对成员合理的访
    问。所以封装本质是一种管理。


    对象——类的实例化

    单纯声明一个类是没有什么意义的,因为声明一个类并没有分配实际的内存空间。

    可以理解为类就是一个设计图,它只设计出需要什么材料和使用材料的方法,并没有建造实体的建筑。

    而拿着图纸盖楼的这个过程就可以理解为用类创建对象。

    image-20220811134707769

    class Person {
    public:
    	void DisplayInfo() {
    		cout << _name << endl;
    		cout << _sex << endl;
    		cout << _age << endl;
    	}
    	//...
    
    	char* _name;
    	char* _sex;
    	int _age;
    	//...
    };
    
    int main() {
    	Person p; //创建对象
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    类对象大小的计算和存储

    先看一组对比:

    //cpp
    class Date_cpp {
    private:
        int _year;
        int _day;
        int _month;
    }
    
    //c
    struct Date_c {
        int _year;
        int _day;
        int _month;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    sizeof(Date_cpp) = 12;

    sizeof(Date_c) = 12;

    实际计算结果如上。

    如果类中加入成员函数呢?

    //cpp
    class Date_cpp {
    public:
        void Init(int year = 2022, int month = 8, int day = 11) {
            _year = year;
            _month = month;
            _day = day;
        }
        //...
        
    private:
        int _year;
        int _day;
        int _month;
    }
    
    //c
    struct Date_c {
        int _year;
        int _day;
        int _month;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    计算出来的结果还是没变。

    实际上,一个类的大小只与它的成员变量有关,且大小计算符合C语言中的结构体内存对齐规则,具体可以看这篇文章【C语言进阶篇】一篇文章带你认清结构、枚举和联合_LeePlace的博客-CSDN博客

    当创建出来一个对象的时候,给对象分配的空间大小实际上就是类的大小,类的成员函数是存放在公共代码区的:

    image-20220811145857280

    那么问题来了,空类的大小是 0 吗?

    image-20220811150201804

    事实上并不是,编译器还是给了一个字节来进行标识,这一个字节并没有实际的意义。

    总结一下:一个类的大小实际就是该类中成员变量之和,当然也要进行内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。


    this指针

    引出

    有下面一段代码:

    class Date {
    public:
    	void Init(int year = 2022, int month = 8, int day = 11) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	void Print() {
    		cout << _year << endl;
    		cout << _month << endl;
    		cout << _day << endl;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main() {
        Date d1;
        Date d2;
        d1.Init();
        d2.Init();
        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

    Init 函数为例,函数想完成对类的初始化,参数全缺省没什么疑问,里面的 _year, _month, _day 是从哪来的呢?成员函数存在公共代码段,并不在对象的存储空间里面,它是怎么知道要操作的成员变量是属于哪个类呢?这就引出了 this 指针。

    C++编译器给每个 “非静态的成员函数” 增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

    所以上述代码的 InitPrint 函数还有一个隐藏的参数 Date* this

    当调用函数的对象是 d1 时, this = &d1

    当调用函数的对象是 d2 时, this = &d2

    我们写代码时写的是 d1.Init()

    编译器处理的实际是 d1.Init(&d1)

    对应的 Init 函数,

    我们写的是void Init(int year = 2022, int month = 8, int day = 11)

    编译器调用时其实是void Init(Date* this, int year = 2022, int month = 8, int day = 11)


    特性

    1. 对于 this 指针,在成员函数内部是无法改变 this 指针的指向的,
      所以更进一步,this 指针的完整类型其实是 Date* const this

    2. this 只能在成员函数内部使用,可以显示使用,编译器默认隐式使用,但隐式使用的时候需要注意,对象的成员名不能和形参名一致,否则编译器会就近处理,导致形参给形参赋值。

      image-20220811152627917

      image-20220811153329051

      但无论显不显示调用,都建议形参和成员变量区分开,建议成员变量的名字前边加一个_ :
      在这里插入图片描述

    3. this 指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储 this 指针。

    4. this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。如果传递了就会报错:

      image-20220811153727111

      即使某些情境下没有报错,那也是进行了隐式类型转换,后续还会导致问题。

      image-20220811153847798


    一道面试题

    有下面一段代码:

    class A {
    public:
        void PrintA() {
            cout<<_a<Show();
        p->PrintA();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 这段代码编译能通过吗?
    2. 这段代码能运行起来吗?如果不能是在哪里崩掉的?

    看到这题第一眼可能就会说编不过,因为 p 是空指针,空指针怎么还能用 -> 呢?

    但实际情况是:

    image-20220811155250993

    这段代码编过了。

    p->Show() 实际上等价于 Show(&p)p 是一个指针变量,有实际的存储空间,也就有地址,所以 &p 是完全合法的操作,那语法上也就没有错误,下面同理。

    那这段代码能运行起来吗?

    image-20220811155854289

    果不其然,挂掉了,但是在 Print 函数内挂掉的,而且此时看控制台已经打印出来了 Show()

    image-20220811160003254

    分析一下,show 函数内部并没有用到 this 指针,所以你给我传过来一个空指针的地址没有关系,反正我又用不到。

    PrintA 函数就不一样了,PrintA 函数内部的那条语句实际上是

    cout << this->_a << endl;

    this 是空指针的地址,

    也就等价于 cout << NULL._a << endl;

    程序自然而然地就崩溃了。

  • 相关阅读:
    矩阵分析与应用
    CSS通用样式3——表格
    【Vue3】手把手教你创建前端项目 Vue3 + Ts + vite + pinia
    数据库(一)
    主打低功耗物联网国产替代,纵行科技ZT1826芯片以速率和灵敏度出圈
    docker Nginx反向代理内网多个服务如:nacos、jenkins、minio等
    sprignboot新依赖nacos,报错一直连接本地的localhost:8848解决
    com.sun.tools.javac.code.TypeTags解决办法
    精通Java事务编程(2)-弱隔离级别之已提交读
    centerOS搭建kafka集群
  • 原文地址:https://blog.csdn.net/Ye_Ming_OUC/article/details/126287408