C++之类和对象(一)

x33g5p2x  于2021-10-10 转载在 C/C++  
字(5.6k)|赞(0)|评价(0)|浏览(502)

面向过程和面向对象

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。但是C不是一个纯面向对象的语言,因为C兼容C,故C既有面向过程,也有面向对象,可以混合编程

类的引入

首先在C语言中,我们是这样定义链表节点的:

  1. //C
  2. struct ListNodeC
  3. {
  4. int val;
  5. struct ListNodeC* next;
  6. };

在C语言中,结构体指针的结构体类型必须写struct ListNodeC/*,而在C++中:

  1. //CPP
  2. struct ListNodeCPP
  3. {
  4. int val;
  5. ListNodeCPP* next;
  6. };

在C中既可以加struct,又可以不加struct,因为C兼容C结构体的语法,同时C中的struct已经不仅仅是结构体,struct已经同时升级成类,定义类,也就是定义出一个新的类型,C语言是面向过程的,数据和方法是分离的,而C面向对象,数据和方法是放在一起的(封装在一起)

在C语言中我们在结构体中只能定义变量,而在C++中我们在结构体中不仅可以定义变量,也可以定义函数:

  1. struct Student
  2. {
  3. void SetStudentInfo(const char* name, const char* gender, int age)
  4. {
  5. strcpy(_name, name);
  6. strcpy(_gender, gender);
  7. _age = age;
  8. }
  9. void PrintStudentInfo()
  10. {
  11. cout<<_name<<" "<<_gender<<" "<<_age<<endl;
  12. }
  13. char _name[20];
  14. char _sex[3];
  15. int age;
  16. };

在C中struct可以定义类,但是C更习惯用class来代替,我们来看使用class进行类的定义

类的定义

在C语言中,我们写栈的实现是这样写的,数据和方法(函数)是分离的:

  1. struct Stack
  2. {
  3. int _a[100];
  4. int top;
  5. int capacity;
  6. };
  7. //方法:
  8. void StackInit(){}
  9. void StackPush(){}
  10. //...

在C++中,引入了类,数据和方法(函数)是封装的:

  1. class Stack
  2. {
  3. void Init(){}
  4. void Push(){}
  5. int _a[100];
  6. int top;
  7. int capacity;
  8. };

class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面有分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

类的两种定义方式

  • 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
  1. class Stack
  2. {
  3. public:
  4. void Init() {}
  5. void Push() {}
  6. public:
  7. int _a[100];
  8. int top;
  9. int capacity;
  10. };

上面这种写法就是将声明和定义全部放在类当中

  • 声明放在.h文件中,类的定义放在.cpp文件中

Stack.h

  1. class Stack
  2. {
  3. //声明
  4. public:
  5. void Init();
  6. void Push();
  7. void Pop();
  8. public:
  9. int _a[100];
  10. int top;
  11. int capacity;
  12. };

Stack.cpp

  1. #include"Stack.h"
  2. void Init()
  3. {
  4. //此处省略定义
  5. }
  6. void Push()
  7. {
  8. //此处省略定义
  9. }
  10. void Pop()
  11. {
  12. //此处省略定义
  13. }

但是函数的定义就这样写可以吗?答案是不可以的

我们首先调用一下这些函数,看能不能正常运行:

  1. #include"Stack.h"
  2. int main()
  3. {
  4. Stack s2;
  5. s2.Init();
  6. s2.Push();
  7. return 0;
  8. }

发现有一个链接错误,为什么呢?

这里说链接错误,因为在链接时找到了Init和Push函数的声明,然后在Stack.cpp生成的目标文件中找函数定义,但是找不到,为什么呢?因为我们定义的类对象s1,s1.Init();编译器找时,不知道哪个是该类的成员函数

那么怎么解决呢?这时就有了类的作用域这个概念

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

故上面的.cpp文件应该这么写:

  1. #include"Stack.h"
  2. void Stack::Init()
  3. {
  4. //此处省略定义
  5. }
  6. void Stack::Push()
  7. {
  8. //此处省略定义
  9. }
  10. void Stack::Pop()
  11. {
  12. //此处省略定义
  13. }

对于类规范性的写法:

如果是短小的成员函数,直接在类里面定义,也就是inline函数

代码比较长的函数,建议声明和定义分离

有人可能看到了在类的定义中我们写了public,那么这又是什么呢?不要着急我们接下来看类的访问限定符

类的访问限定符和封装

访问限定符

数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。C++用类来实现封装,类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。

访问修饰符

  • public
  • protected
  • private
  1. class Stack
  2. {
  3. //公有
  4. public:
  5. void Init(){}
  6. void Push(){}
  7. //私有
  8. private:
  9. int _a[100];
  10. int top;
  11. int capacity;
  12. };
  13. int main()
  14. {
  15. Stack s2;
  16. s2.Init();
  17. s2.Push();
  18. return 0;
  19. }

想让你在类外进行访问的设为公有,不想让你在类外进行访问的设为私有

注意:

当我们不写访问限定符时默认为私有

我们现在已经知道struct在C++中既可以当成结构体用也可以当作类来用

那么class和struct不写访问限定符默认都为私有吗?
class不写访问限定符默认为私有

struct不写访问限定符默认公有

封装

我们在刚开始学习C++的时候,经常听说面向对象的三大特性:封装、继承、多态

那么什么是封装呢?这里我们讲一下封装:
类的定义和设计就体现了封装,数据和方法是放在一起的。在C中不封装,比较自由(用的人比较懂规范,能够恪守规则),随便访问数据和方法,CPP中想给你访问的,定义为公有,不想给你访问的,定义私有(强制你恪守规则),所以封装是更好的

类的实例化

一个类可以实例化出多个对象,用类类型创建对象的过程,称为类的实例化,

注意:

在我们在写一个类时,里面写的成员变量其实是成员变量的声明,成员变量的声明其实告诉你变量的类型、名称、这里是没有没开辟空间来存储它的,成员变量是属于对象的,对象在被实例化出时,才是他们定义的地方,定义和初始化是没有关系的,变量和对象的定义,应该是开辟内存空间存放他。

比如房子的设计图,不能住人,实例化就像用图纸建造房子,房子才能住人,房子设计图就像类,类里面不能存数据,类实例化对象,对象才能存数据

  1. class Person
  2. {
  3. public:
  4. //显示基本信息
  5. void showInfo()
  6. {
  7. cout << _name <<" "<< _sex <<" "<< _age << endl;
  8. }
  9. public:
  10. const char* _name;//姓名
  11. const char* _sex;//性别
  12. int _age;//年龄
  13. };
  14. void Test()
  15. {
  16. Person p1;
  17. p1._name = "zhangsan";
  18. p1._age = 10;
  19. p1._sex = "男";
  20. p1.showInfo();
  21. }
  22. int main()
  23. {
  24. Test();
  25. return 0;
  26. }

Person p1就是实例化出一个对象

如何计算类对象的大小

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

  1. class A
  2. {
  3. public:
  4. void PrintA()
  5. {
  6. cout<<_a<<endl;
  7. }
  8. private:
  9. char _a;
  10. };

类实例化出对象,那么该对象是怎么存储的呢?

类对象的存储方式

假如对象中包含类的各个成员:

这样存储是有缺陷的:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?

  • 只保存成员变量,成员函数存放在公共的代码段

那么计算机是安装哪种方式存储的呢?

事实上是按照第二种方式进行存储的

  1. class A
  2. {
  3. public:
  4. void PrintA()
  5. {
  6. cout<<_a<<endl;
  7. }
  8. private:
  9. char _a;
  10. };

那么sizeof(A)的结果是什么呢?

计算类大小,或者类对象的大小,只考虑成员变量,因为对象中,只存了成员变量,没有存成员函数,一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐。这里A的大小其实就是1。

不了解内存对齐的读者请前往:结构体内存对齐

那么下面的两个类的大小又是多少呢?

  1. //没有成员变量的类
  2. class B
  3. {
  4. public:
  5. void print()
  6. {}
  7. };
  1. class C
  2. {};//空类

我们发现B是一个没有成员变量的类,C是一个空类,那么没有成员变量的类和空类大小是多少呢?答案是1字节,如果一个类没有成员,那么它的对象需要给1字节进行占位表示对象存在,这1字节不存储有效数据

this指针

我们首先写一个日期类:

  1. class Date
  2. {
  3. public :
  4. void Display ()
  5. {
  6. cout <<_year<< "-" <<_month << "-"<< _day <<endl;
  7. }
  8. //void SetDate(Date* this,int year , int month , int day)
  9. void SetDate(int year , int month , int day)
  10. {
  11. _year = year;
  12. //this->_year = year;
  13. _month = month;
  14. //this->_month = month;
  15. _day = day;
  16. //this->_day = day;
  17. }
  18. private :
  19. int _year ; // 年
  20. int _month ; // 月
  21. int _day ; // 日
  22. };
  23. int main()
  24. {
  25. Date d1;
  26. Date d2;
  27. d1.Init(2021,10,8);
  28. //d1.Init(&d1,2021,10,8);
  29. d2.Init(2022,10,8);
  30. d1.Display();
  31. d2.Display();
  32. return 0;
  33. }

Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,如果创建了d1,d2两个对象,那当d1调用SetDate函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

实际上这里有一个this指针,它指向当前对象,通过它可以访问当前对象的所有成员。我们在调用成员函数后,编译器其实会再传一个参数进去,这个参数为当前对象的地址:

  1. d1.Init(&d1,2021,10,8);

而成员函数那边用一个this指针来接收,实际上SetDate成员函数在编译时会成为这样:

  1. void SetDate(Date* this,int year , int month , int day)
  2. {
  3. this->_year = year;
  4. this->_month = month;
  5. this->_day = day;
  6. }

这样我们就区分了到底设置哪个对象的日期。

注意:对象可以调用成员函数,成员函数中也可以调用成员函数

那么this指针存储在哪里呢?

常犯的错误:this存储在对象里面

this指针是形参,形参和函数中的局部变量都是存在函数栈帧里面的,所以this指针是存在栈区的,vs下this指针是通过寄存器ecx传递的,看下图:

下面我们来看一个面试题:

  1. class A
  2. {
  3. public:
  4. void show()
  5. {
  6. cout<<"show()"<<endl;
  7. }
  8. void Print()
  9. {
  10. cout<<_a<<endl;
  11. }
  12. private:
  13. int _a;
  14. };
  15. int main()
  16. {
  17. A* p = nullptr;
  18. p->show();
  19. return 0;
  20. }

该代码的运行结果是什么呢?

1、正常运行

2、编译不通过
这里会正常运行,因为成员函数的地址不在对象中存储,存在公共代码段,那么这里调用成员函数,不会去访问p指向的空间,也就不存在空指针解引用了,调用show函数只会在存储成员函数的公共区域里找这个函数,这里只会把p传递给隐含的this指针,但是show函数中也没有解引用this指针

那么去调用Print函数程序的运行结果是什么呢?

  1. void Print(A* this)
  2. {
  3. cout<<this->_a<<endl;//这里进行了访问p所指向的空间,所以会编译不通过
  4. }
  5. int main()
  6. {
  7. A* p = nullptr;
  8. p->Print();
  9. return 0;
  10. }

相关文章

最新文章

更多