目录
一、面向过程和面向对象
二、类
1、定义
2、类的两种定义方式
3、访问限定符
4、命名规范化
5、类的实例化
6、计算类对象的大小
7、存储方式
三、this指针
1、定义
2、存储位置
3、辨析
四、封装好处
面向过程和面向对象是两种主要的编程范式。它们的主要区别在于如何组织代码和处理数据。
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语言中需用用户自己维护。