目录
面向过程和面向对象是两种主要的编程范式。它们的主要区别在于如何组织代码和处理数据。
1. 面向过程编程:
面向过程编程是一种编程范式,它以过程(也称为函数)为中心,数据和过程是分开的。在面向过程编程中,程序是一系列被调用的函数或过程,数据被视为辅助函数运行的附属物。这种编程范式的主要特点是流程化,按照事物处理的逻辑顺序来编写程序。
例如,假设我们要编写一个程序来制作一杯咖啡。在面向过程的编程中,我们会创建一个函数来热水,一个函数来研磨咖啡豆,一个函数来混合水和咖啡,等等。每个函数都有一个明确的任务,并按照特定的顺序执行。
2. 面向对象编程:
面向对象编程是一种编程范式,它将数据和处理数据的函数组合成一个整体,称为“对象”。在面向对象编程中,程序是由对象组成的。每个对象都包含数据(称为属性)和一组处理数据的函数(称为方法)。这种编程范式的主要特点是封装性,继承性和多态性。
还是以制作咖啡为例,面向对象编程会创建一个“咖啡”对象,这个对象有一些属性(如水,咖啡豆等)和一些方法(如热水,研磨咖啡豆,混合水和咖啡等)。所有的操作都是通过操作对象的方法来完成的。
C语言结构体中只能定义变量,而在C++中,结构体不仅可以定义变量,也可以定义函数。
在C++中把结构体升级成了类,而且结构体的名字就是类型。
- //C++兼容C结构体用法
- typedef struct ListNode
- {
- int val;
- //C struct ListNode是类型
- struct ListNode* next;
- }LN;
-
- struct ListNode
- {
- int val;
- //C++ ListNode是类型
- ListNode* next;
- };
- class className
- {
- // 类体:由成员函数和成员变量组成
-
- }; // 一定要注意后面的分号
在之前数据结构中学过的栈,因为用C语言实现,结构体中只能定义变量,现在我们可以通过C++的方式实现栈,这样在结构体中也可以定义函数。
第一种:声明和定义全部放在类体中
注意:
这里出现了public和private,他们是访问限定符,稍后进行讲解。
- class Stack
- {
- public:
- // 成员函数
- void Init(int n = 4)//缺省参数
- {
- a = (int*)malloc(sizeof(int) * n);
- if (nullptr == a)
- {
- perror("malloc fail");
- return;
- }
- capacity = n;
- size = 0;
- }
- void Push(int x)
- {
- //...
- a[size++] = x;
- }
- private:
- // 成员变量
- int* a;
- int size;
- int capacity;
- };
- int main()
- {
- Stack st;
- st.Init();
- st.Push(1);
- st.Push(2);
- st.Push(3);
- return 0;
- }
第二种:类声明放在.h文件中,成员函数定义放在.cpp文件中(常用这种)
头文件中:
- //struct Stack无需访问限定符
- class Stack//必须有访问限定符,否则报错
- {
- public:
- // 成员函数
- void Init(int capacity = 4);
- void Push(int x);
-
- private:
- // 成员变量
- int* a;
- int size;
- int capacity;
- };
源文件中:
函数名前要加类名 : : (域作用限定符)。
- #include "Stack.h"
-
- void Stack::Init(int n)
- {
- a = (int*)malloc(sizeof(int) * n);
- if (nullptr == a)
- {
- perror("malloc申请空间失败");
- return;
- }
-
- capacity = n;
- size = 0;
- }
-
- void Stack::Push(int x)
- {
- //...
- a[size++] = x;
- }
在头文件中使用 stuct 定义类:
- struct Stack
- {
- void Init(int capacity = 4);
- void Push(int x);
-
- int* a;
- int size;
- int capacity;
- };
成功编译:
在头文件中使用 class 定义类:
- class Stack
- {
- void Init(int capacity = 4);
- void Push(int x);
-
- int* a;
- int size;
- int capacity;
- };
结果编译后报错如下:
我们可以发现成员函数无法访问类的成员,这是因为未加访问限定符的class中默认访问权限为私有private,stuct 默认为公有public。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问权限符说明:
我们来看这个代码:
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- year = year;
- month = month;
- day = day;
- }
- private:
- int year;
- int month;
- int day;
- };
在这个代码中,存在一个问题。在Init
函数中,你试图将传入的参数赋值给类的成员变量,但是由于参数和成员变量的名称相同,会导致赋值操作无效。
我们可以选择修改参数名或类成员名,比较好的是在成员名前加上符号_ ,这样小改动保证成员名和函数参数名之间的关系。
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
用类类型创建对象的过程,称为类的实例化
类中成员变量是声明,没有分配空间。
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
只有当我们使用类创建一个对象也就是”类的实例化“时,成员变量才会被分配空间,存储成员变量。
- int main()
- {
- Date d1;//实例化
- d1.Init(2023, 2, 3);
- return 0;
- }
我们通过下面这段代码看看类实例化之后,有没有占用实际的物理空间。
- class A2 {
- public:
- void f2() {}
- };
-
- int main()
- {
- A2 aa1;
- A2 aa2;
-
- //实例化就能打印地址
- cout << &aa1 << endl;
- cout << &aa2 << endl;
-
- return 0;
- }
通过打印地址可以发现,实例化后变量确实被分配空间用来存储类成员变量了。
类的大小也遵循与结构体一样的计算方式,详细请看这篇文章:结构体内存对齐
我们先来看下面这段代码:
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
-
- Date d1;
- d1.Init(2023, 2, 2);
-
- //打印d1的大小
- cout << sizeof(d1) << endl;
-
- return 0;
- }
我们由输出结果可知: d1的大小为12字节,由此可以推断出类的对象d1中只存储了成员变量。
那么为什么成员变量在对象中,成员函数不在对象中呢?
因为每个对象成员变量是不一样的,需要独立存储;每个对象调用成员函数是一样的,它们被放到共享公共区域(代码段) 。
- class A2 {
- public:
- void f2() {}
- };
-
- int main()
- {
- A2 a;
- cout << sizeof(a) << endl;
-
- return 0;
- }
类中只有成员函数,这样定义的对象A2大小是1字节,这1字节不存储有效数据,是用来占位的,标识对象被实例化定义出来了。
三种类的大小对比
- // 类中既有成员变量,又有成员函数
- class A1 {
- public:
- void f1(){}
- private:
- int a;
- };
-
- // 类中仅有成员函数
- class A2 {
- public:
- void f2(){}
- };
-
- // 类中什么都没有---空类
- class A3
- {};
- 一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
- 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
- class Date{
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- cout << this << endl;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1;
- Date d2;
- d1.Init(2022, 2, 2);
- d2.Init(2023, 2, 2);
-
- return 0;
- }
Date类中有 Init 成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
我们在初始化Init函数中打印this指针,看看this指针有没有发挥作用。
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- cout << this << endl;
- //可以显式使用this,不能显式定义this
- this->_year = year;
- this->_month = month;
- this->_day = day;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1;
- Date d2;
- d1.Init(2022, 2, 2);
- d2.Init(2023, 2, 2);
-
- return 0;
- }
输出了两个地址,我们通过调试检查一下这两个地址是不是对象d1和d2的地址。
在调试中地址如下,上下两幅图对比可以得知,每次初始化时,this指针会指向参数的地址。
在实际中类的成员函数一般不需要加this指针。
this存在哪里?
栈,因为他是隐含形参,vs下在ecx寄存器中。
通过之前计算类的对象大小不包括this,所以this在栈上,它是一个隐含的形参。
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- cout << this << endl;
- //可以显式使用this,不能显式定义this
- this->_year = year;
- this->_month = month;
- this->_day = day;
- }
-
- void func()
- {
- cout << "func()" << endl;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date* ptr = nullptr;
- ptr->func();
-
- return 0;
- }
程序正常运行:
如果调用这句呢?
ptr->Init(2022, 2, 2);
结果运行崩溃:
下面来解释这两种情况:
ptr->func()
:这行代码尝试通过空指针ptr
调用成员函数func()
。虽然ptr
是空指针,但是在这种情况下,由于func()
函数没有使用或修改任何成员变量,它可以被静态调用。这意味着它不依赖于具体的对象实例,不需要借助this指针,因此不会引发崩溃。
ptr->Init(2022, 2, 2)
:这行代码尝试通过空指针ptr
调用成员函数Init()
。由于Init()
函数内部使用了this
指针来访问对象的成员变量,而空指针没有有效的对象实例,对空指针的解引用无效,因此在访问this->_year
、this->_month
和this->_day
时会导致程序崩溃。
同理这段代码也可以正常运行:
(*ptr).func();
C++中:
C语言中:
C++实现栈:
- typedef int DataType;
- class Stack
- {
- public:
- void Init()
- {
- _array = (DataType*)malloc(sizeof(DataType) * 3);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
- _capacity = 3;
- _size = 0;
- }
- void Push(DataType data)
- {
- CheckCapacity();
- _array[_size] = data;
- _size++;
- }
- void Pop()
- {
- if (Empty())
- return;
- _size--;
- }
- DataType Top() { return _array[_size - 1]; }
- int Empty() { return 0 == _size; }
- int Size() { return _size; }
- void Destroy()
- {
- if (_array)
- {
- free(_array);
- _array = NULL;
- _capacity = 0;
- _size = 0;
- }
- }
- private:
- void CheckCapacity()
- {
- if (_size == _capacity)
- {
- int newcapacity = _capacity * 2;
- DataType* temp = (DataType*)realloc(_array, newcapacity *
- sizeof(DataType));
- if (temp == NULL)
- {
- perror("realloc申请空间失败!!!");
- return;
- }
- _array = temp;
- _capacity = newcapacity;
- }
- }
- private:
- DataType* _array;
- int _capacity;
- int _size;
- };
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack * 参数是编译器维护的,C语言中需用用户自己维护。