C++类与对象(1)—初步认识
目录
一、面向过程和面向对象
二、类
1、定义
2、类的两种定义方式
3、访问限定符
4、命名规范化
5、类的实例化
6、计算类对象的大小
7、存储方式
三、this指针
1、定义
2、存储位置
3、辨析
四、封装好处
一、面向过程和面向对象
- C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
- C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
面向过程和面向对象是两种主要的编程范式。它们的主要区别在于如何组织代码和处理数据。
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;
};
1、定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
- class为定义类的关键字(C语言中用struct),ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
- 类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
2、类的两种定义方式
在之前数据结构中学过的栈,因为用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;
}
3、访问限定符
在头文件中使用 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++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问权限符说明:
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
4、命名规范化
我们来看这个代码:
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;
};
5、类的实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间、存储类成员变量。
类中成员变量是声明,没有分配空间。
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;
}
通过打印地址可以发现,实例化后变量确实被分配空间用来存储类成员变量了。
6、计算类对象的大小
类的大小也遵循与结构体一样的计算方式,详细请看这篇文章:结构体内存对齐
7、存储方式
我们先来看下面这段代码:
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
{};
- 一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
- 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
三、this指针
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对象呢?
1、定义
我们在初始化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指针。
2、存储位置
this存在哪里?
栈,因为他是隐含形参,vs下在ecx寄存器中。
通过之前计算类的对象大小不包括this,所以this在栈上,它是一个隐含的形参。
3、辨析
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语言中需用用户自己维护。