c++类与对象
前言
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
一、
1、类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。
struct Stack
{
void Init(int n)
{
a = (int*)malloc(sizeof(int) * n);
if (nullptr == a)
{
perror("malloc申请空间失败n");
return;
}
capacity = n;
size = 0;
}
void Push(int x)
{
if ((size == capacity) && (size != 0))
{
a = (int*)realloc(a,sizeof(int) * (2*capacity));
}
a[size] = x;
size++;
}
int* a;
int size;
int capacity;
};
int main()
{
Stack s;
s.Init(4);
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
s.Push(5);
return 0;
}
2、类的定义
虽然在c++中可以使用struct来定义类,但是在C++中更喜欢用class来代替。
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
class className
{
//类体:由成员函数和成员变量组成
}; //一定要注意后面的分号
类的两种定义方式:
(1). 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Stack
{
public:
void Init(int n = 4)
{
a = (int*)malloc(sizeof(int) * n);
if (nullptr == a)
{
perror("malloc申请空间失败n");
return;
}
capacity = n;
size = 0;
}
void Push(int x)
{
if ((size == capacity) && (size != 0))
{
a = (int*)realloc(a,sizeof(int) * (2*capacity));
}
a[size] = x;
size++;
}
private:
int* a;
int size;
int capacity;
};
int main()
{
Stack s;
s.Init(4);
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
s.Push(5);
return 0;
}
(2). 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::。因为类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
需要注意的是在定义类成员函数时要使用Stack::,即告诉编译器Init方法不是一个全局方法,是在Stack类中的方法。
3、类的访问限定符及封装
在上面定义类的成员函数时,我们可以发现在前面加上了public:修饰,如果不加的话就会显示该类的成员函数不可以访问。这时因为类的访问限定问题。例如如果我们创建了一个类,该类有些重要的函数我们不希望在其他地方被调用,此时我们就可以使用类的访问限定符来修饰该函数。用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符分为三类:
(1). public (公有)
(2). protected (保护)
(3). private (私有)
访问限定符说明:
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
所以我们就可以理解为什么前面写的代码中需要在类的成员函数前面加上public修饰,因为类中的默认访问权限都为private,即在外面不能访问类的成员函数,所以要将访问权限改为public,这样才能在外面访问到这些成员函数。
4、类的实例化
用类类型创建对象的过程,称为类的实例化。
(1). 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没
有分配实际的内存空间来存储它。
(2). 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
可以理解为类的定义就是设计出一个楼房的设计图,此时只有一个设计图,该设计图不占用土地,即类的定义不占用空间,类的实例化就是根据这个设计图来盖出来一栋楼房,此时该楼房占用了土地。一个类可以实例化多个对象就是根据这一个设计图可以盖出来很多楼房。
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._year; //因为此时没有给该类开辟空间
// 类对象实例化 -- 开空间
Date d1;
Date d2;
//d1._year = 200; //因为该Date类的属性都为private,所以不可以在外面修改
d1.Init(2023, 9, 8);
return 0;
}
5、类对象模型
一个类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
可以看到使用sizeof()来计算类的大小只是计算了类的成员变量的大小,并没有计算类的成员函数的大小。
为什么需要这样存储呢?这是因为每个对象中成员变量的值是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类实例化多个对象时,每个对象中都会保存一份函数代码,相同代码保存多次,浪费空间。所以在类实例化为对象时,每个对象的成员变量不一样,所以独立存储,但是每个对象调用的成员函数是一样的,所以将成员函数放到共享公共区域(代码段)。
下面的图片中可以看到当类中没有成员变量时,该类也占用了1个字节空间,这1字节空间并不存储有效数据,而是占位用,标识该对象被实例化定义了出来。因为如果不标识的话,如果调用A2中的f2方法就没法调用了,因为根本没有空间,不知道在哪里调用。
结论:
一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
6、this指针
class Date
{
public:
//成员函数
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
//成员变量/类的属性
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023, 9, 8);
d2.Init(2021, 6, 6);
d1.Print();
d2.Print();
return 0;
}
通过上面的代码和运行结果我们可以看到不同的对象调用Print()方法打印出来的数据是不一样的,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性:
(1). this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
(2). 只能在“成员函数”的内部使用。
(3). this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
(4). this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
那么this指针存在哪里呢?
答:栈里面,因为this指针实际就是类成员函数的形参,而当调用函数时,函数的形参都存在栈里面,所以this指针也在栈里面,this指针本质就是一个类指针,只不过该指针不需要我们自己定义,而是编译器自动给我们增加的一个隐藏指针参数,我们只需要知道this指针的存在,然后使用它即可。
空指针问题:
在下面的代码中,对空指针调用func()函数可以正常执行,而对空指针调用Init和Print函数会报空指针异常,这是因为在Init函数中对this指针进行了解引用,而this指针此时是一个nullptr空指针,所以对空指针解引用会报错。而ptr->func()和(*ptr).func()不报错是因为没有对该空指针进行解引用,编译器解引用时会先判断右边要访问的内容在不在这个对象里面,如果在这个对象里面才会解引用,而不在这个对象里面就不会进行解引用,而func()函数的本质不在对象里面,所以没有解引用。
class Date
class Date
{
public:
//成员函数
//void Init(Date* this, int year, int month, int day)
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
//void Print(Date* this)
void Print()
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
void func()
{
cout << "func()" << endl;
}
private:
//成员变量/类的属性
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023, 9, 8);
d2.Init(2021, 6, 6);
d1.Print();
d2.Print();
Date* ptr = nullptr;
ptr->func(); //正常运行
(*ptr).func(); //正常运行
ptr->Init(2023, 3, 4); //报错
return 0;
}
7、封装
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来
和对象进行交互。
例如在c语言中我们实现栈操作时,数据和方法是分离的,即在使用时,我们先需要创建一个Stack结构体变量,然后给该变量初始化,然后调用一系列方法对栈进行操作,并且这些方法什么位置都可以调用,可以在任何时候使用int top = StackTop(&st)来获取栈的栈顶元素,这样是不安全的。
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* data;
int top;
int capacity;
}Stack;
void StackInit(Stack* ps);
void StackPush(Stack* ps, STDataType x);
void StackPop(Stack* ps);
bool StackIsEmpty(Stack* ps);
STDataType StackTop(Stack* ps);
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
void StackInit(Stack* ps)
{
assert(ps);
ps->data = NULL;
ps->top = 0;
ps->capacity = 0;
}
void CheckCapacity(Stack* ps)
{
assert(ps);
int newCapacity = ps->capacity = 0 ? 4 : ps->capacity * 2;
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->data, newCapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->data = tmp;
ps->capacity = newCapacity;
}
}
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
CheckCapacity(ps);
ps->data[ps->top] = x;
ps->top++;
}
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
ps->top--;
}
bool StackIsEmpty(Stack* ps)
{
assert(ps);
if (ps->top == 0)
{
return true;
}
return false;
}
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
return ps->data[ps->top - 1];
}
#define _CRT_SECURE_NO_WARNINGS
#include"Stack.h"
int main()
{
Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPush(&st, 6);
printf("%dn", StackTop(&st));
StackPop(&st);
printf("%dn", StackTop(&st));
return 0;
}
而在c++中栈可以这样实现。
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* data;
int top;
int capacity;
}Stack;
void StackInit(Stack* ps);
void StackPush(Stack* ps, STDataType x);
void StackPop(Stack* ps);
bool StackIsEmpty(Stack* ps);
STDataType StackTop(Stack* ps);
#include"Stack.h"
void StackInit(Stack* ps)
{
assert(ps);
ps->data = NULL;
ps->top = 0;
ps->capacity = 0;
}
void CheckCapacity(Stack* ps)
{
assert(ps);
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->data, newCapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->data = tmp;
ps->capacity = newCapacity;
}
}
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
CheckCapacity(ps);
ps->data[ps->top] = x;
ps->top++;
}
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
ps->top--;
}
bool StackIsEmpty(Stack* ps)
{
assert(ps);
if (ps->top == 0)
{
return true;
}
return false;
}
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(!StackIsEmpty(ps));
return ps->data[ps->top - 1];
}
#include"Stack.h"
int main()
{
Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPush(&st, 6);
printf("%dn", StackTop(&st));
StackPop(&st);
printf("%dn", StackTop(&st));
return 0;
}