【C++】类和对象1
本篇文章主要介绍类与对象的基础知识 -- 类的概念与定义、对象的概念与实例化、this指针。
目录
1 类的概念和定义
1) 类的概念
2) 类的定义
3) 访问限定符
4) class 与 struct 定义类的区别
5) 类域
2 对象的概念与实例化
1) 对象的概念
2) 对象的实例化
(1) 实例化对象的概念
(2) 对象的大小
3 this 指针
4 用 C++ 实现 Stack
1 类的概念和定义
1) 类的概念
类是用户自定义的一种数据类型,是对具有相同属性和行为的一组对象的抽象表示。比如程序员可以自己定义一个学生类,学生包含相同的姓名、成绩以及年龄等一些相同属性,还包括了考试、学习等相同的方法。用这个类定义的一个实体就叫做对象,每个对象都是独立的,每个都具有独立的姓名、成绩以及变量等属性,都可以完成考试、学习等行为。
类是面向对象编程的核心概念,体现了面向对象变成语言的三大特性(封装、多态、继承)中的封装特性,即把该类定义出的对象具有的属性以及方法具体的实现细节隐藏起来,只将方法如何使用暴露在外面,用户只需了解该类的方法的功能以及如何使用即可,简化了用户的使用。
2) 类的定义
类的定义需要用到关键字 class ,定义类的语法格式如下:
class 类名
{//可以定义成员函数void Print(int x){cout << x << endl;}//可以定义成员变量int _a;int _b;
};//分号不能省略
类不同于 C语言中的结构体,只能在结构体定义变量;C++的类中也可以定义函数,称之为成员函数(方法也是成员函数),类中的变量称之为类的属性或者成员变量;且在C++中类名就代表了类型,不需要像C语言中一样在前面加上 struct。
为了区分普通变量与成员变量,一般成员变量会以 '_' 或者 'm' 开头。
C++ 中的类也可以用 struct 关键字定义,但是 class 定义类与 struct 定义类是有区别的(区别我们在讲解完访问限定符之后再讲解),一般情况下建议使用 class 定义类。定义在类中的函数默认为内联函数(inline 函数)。
#include<iostream>using namespace std;//struct也可定义类
//A就代表类型,不需要struct A代表类型了
struct A
{//成员函数int Print(int a){cout << a << endl;}//成员变量int _a1;int _a2;
};//分号不能省略
3) 访问限定符
C++中为了实现封装,用类将对象的属性和方法封装在一起,让对象更加完善,且可以通过访问限定符来修改访问权限,将其接口提供给外部的用户使用。
C++中的类共有3个访问限定符:
(1) public:修饰的成员变量或者成员函数在类外可以直接访问。
(2) private:修饰的成员变量和成员函数只能在类里面访问,不能在类外直接访问。
(3) protected:在类和对象中与 private 限定符没有什么区别,但是在继承中会有区别。
#include<iostream>using namespace std;class A
{
//public限定符修饰可以在类外访问
public:int Print1(int a){cout << a << endl;}void Print2(){//public与protected限定符修饰的变量可以在类里面访问cout << _a1 << ' ' << _a2 << ' ' << _a3 << ' ' << _a4 << endl;}//private限定符修饰不能在类外访问
private:int _a1;int _a2;
//protected限定符在类外不能访问
protected:int _a3;int _a4;
};int main()
{A a;//public可以在类外访问a.Print1(11);a.Print2();//private限定符在类外不可访问//cout << a._a1 << ' ' << a._a2 << endl;//protected限定符在类外不可访问//cout << a._a3 << ' ' << a._a4 << endl;return 0;
}
运行结果:
输出:
11
-858993460 -858993460 -858993460 -858993460
通过输出结果可以看到,Print2函数里面打印的 _a1、_a2、_a3、_a4 都是随机值,这个将会在类与对象 2 之构造函数中讲解。
访问限定符还有几个需要的点:
(1) 访问权限作用符从该限定符出现的位置开始一直直到下一个限定符出现或者直到类的 } 为止(类域结束为止)。
(2) 一般成员变量都会使用 public 或者 protected 修饰,而成员函数一般都会用 public 修饰,因为封装都是希望将该类的接口与方法暴露在外面,而不希望将具体的实现细节暴露在外面。
4) class 与 struct 定义类的区别
class 与 struct 关键字定义类的唯一区别就是在 class 类里面如果不加访问限定符,默认是用 private 限定符修饰的,而 struct 是默认用 public 限定符修饰的:
#include<iostream>using namespace std;struct A
{void Print(int a){cout << a << endl;}
}class B
{void Print(int b){cout << b << endl;}
}int main()
{A a;//struct默认是public修饰的,可以在类外访问成员函数和成员变量a.Print(11);B b;//class默认是private修饰的,不可以在类外访问成员函数和成员变量b.Print(20);return 0;
}
5) 类域
与函数一样,类会定义出一个新的作用域 -- 类域,类的所有成员都在类的作用域中,在类外定义类的成员函数时,需要指定类域(需要用到域作用限定符::),否则编译器会将其当作全局函数处理:
#include<iostream>using namespace std;class Stack
{
public://成员函数//在类里声明//缺省值必须在声明时给void Init(int n = 4);
private://成员变量int* _arr;size_t _capacity;size_t _top;
};//在类外定义类里面的函数
//声明与定义分离时,定义不能给缺省值
//需要指定类域
//不是修饰类型
//Stack::void Init(int n)(x)
//而是修饰函数名
void Stack::Init(int n)
{_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc fail!\n");return;}_top = 0;_capacity = n;
}int main()
{Stack st;st.Init();return 0;
}
在上面的Stack类里面,实现了 Init 函数的声明与定义分离,在类里面声明了 Init 函数,在类外定义了 Init 函数,在类外定义 Init 函数时,需要在 Init 函数名前加上类的名字来指定该函数属于哪个类(注意是限定函数名,而不是限定类型),如果不指定类域,Init 函数就会被当作全局函数,在函数中并没有定义 _arr、_top 与 _capacity 变量,是会报错的:
#include<iostream>using namespace std;class Stack
{
public://成员函数void Init(int n = 4);
private://成员变量int* _arr;size_t _capacity;size_t _top;
};//不指定类域会报错
void Init(int n)
{_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc fail!\n");return;}_top = 0;_capacity = n;
}int main()
{Stack st;st.Init();return 0;
}
运行结果:
2 对象的概念与实例化
1) 对象的概念
在 C++ 中,对象是面向对象编程的核心概念,它将类的属性(成员变量)以及方法(成员函数)封装在一起,形成一个独立的实体。在类和对象中,类就像一个模板或者蓝图,定义了对象所具有的属性和方法,而对象就像是根据这个蓝图所创建出来的实例,每个对象都是相互独立的。
例如定义了一个 Student 类,具有名字、性别、学号与成绩属性,具有考试、学习方法,然后用 Student 类定义出了两个对象,分别是 s1 与 s2,s1 与 s2 共同具有名字、性别、学好与成绩属性和考试、学习方法,但是 s1 与 s2 又是相互独立的,每一个对象又具有不同的名字、性别等属性。
2) 对象的实例化
(1) 实例化对象的概念
类是对象的一种抽象表示,其限定了对象中有哪些属性以及方法,但是类的定义并不会开辟物理空间,只有当用该类类型定义出对象时,对象才会占用物理空间。类就好像一个房子的图纸一样,定义出类也就相当于画出了房子的图纸,并不会占用实际的物理空间,只有用该图纸建造出实际的房子之后才会占用物理空间,这里用图纸建造出房子就相当于用类定义出了对象。像上述用类定义出对象,也就是用类类型在物理内存中创建出对象的过程,称为类实例化对象。
和用图纸造房子一样,一个类是可以实例化出多个对象的,每个对象独立的拥有各自的物理空间,存储类的成员变量。
#include<iostream>using namespace std;class Date
{
public:void Init(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << '-' << _month << '-' << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{//用Date类实例化出对象d1与d2//每个对象独立的拥有一块物理空间,存储_year、_month、_dayDate d1, d2;//可以调用public限定符修饰的方法d1.Init(2025, 1, 1);d1.Print();d2.Init();d2.Print();return 0;
}
运行结果:
输出:
2025-1-1
1-1-1
(2) 对象的大小
在讲解对象的大小之前,我们先分析一下对象中含有什么成员。从实例化对象来看,每个对象都具有独立的属性,所以每个对象里面肯定是含有相应的属性的存储空间的,但是对象也可以调用对应的方法(成员函数),那么成员函数是否存在对象中呢?我们先来看个例子:
#include<iostream>using namespace std;class Date
{
public:void Init(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << '-' << _month << '-' << _day << endl;}
private:int _year;int _month;int _day;
}int main()
{Date d1;cout << sizeof(d1) << endl;return 0;
}
运行结果:
输出:
12
可以看出 Date 类对象的大小为 12 字节,而 Date 类中包含 2 个成员函数与 3 个整型的成员变量,而 3 个整型的大小正好是 12 字节,所以对象中是不包含成员函数的。
其实成员函数也会像普通函数一样,在编译时被编译为一串指令,然后通过地址去调用该函数,进而达到调用成员函数的作用(在编译阶段被转换为指令 [call 成员函数地址])。
所以对象中只是包含类中的成员变量,而不会包含成员函数。但是与 C 语言中的结构体类似,对象中的成员变量也符合内存对齐的规则(不知道大家还记不记得内存对齐):
(1) 第一个成员对齐在与结构体偏移量为0的地址处。
(2) 其他变量对齐到对齐数的整数倍的地址处。
(3) 结构体的总体大小为最大对齐数的整数倍。
(4) 如果起嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小对齐到最大对齐数的整数倍处。
#include<iostream>using namespace std;struct A
{char _a1;int _a2;
};int main()
{cout << sizeof A << endl;return 0;
}
运行结果:
输出:
8
我们再来看一个嵌套的情况:
#include<iostream>using namespace std;struct A
{int _a1;char _a2;
};struct B
{int _b1;A _a;
};int main()
{cout << sizeof B << endl;return 0;
}
运行结果:
输出:
12
3 this 指针
由实例化对象可以知道,一个类是可以创建出多个对象的,那么调用成员函数的时候具体是哪个对象调用的就会分不清,C++ 中引入了 this 指针来解决这一问题。
在编译器编译后,每个成员函数都会在前面隐含一个 this 指针,且在成员函数内部使用成员变量或者调用其他成员函数的时候前面也会隐含一个 this 指针,如:
#include<iostream>using namespace std;class Date
{
public:void Init(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << '-' << _month << '-' << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1,d2;d1.Init();d2.Init(2025, 1, 1);d1.Print();d2.Print();return 0;
}
在展开 this 指针后会变成如下代码:
#include<iostream>using namespace std;class Date
{
public:void Init(Date* const this, int year = 1, int month = 1, int day = 1){this->_year = year;this->_month = month;this->_day = day;}void Print(Date* const this){cout << this->_year << '-' << this->_month << '-' << this->_day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1,d2;d1.Init(&d1);d2.Init(&d2, 2025, 1, 1);d1.Print(&d1);d2.Print(&d2);return 0;
}
且成员函数中的传递的 this 指针类型为 Date* const this,代表 this 指针本身是不能改变的,但是指向的值却可以改变:
#include<iostream>using namespace std;class A
{
public:void Print(){//编译报错,因为是 A* const thisthis = nullptr;cout << _a1 << ' ' << _a2 << endl;}private:int _a1;int _a2;
};int main()
{A a;a.Print();return 0;
}
运行结果:
在 C++ 中规定在函数的形参与实参位置是不能显示的写 this 指针的,但是可以在函数体中显示使用 this 指针:
#include<iostream>using namespace std;class A
{
public://形参不能显示写this指针//void Print(A* const this)(x)void Print(){//可以显示的使用 this 指针this->_a1 = 1;this->_a2 = 2;cout << _a1 << ' ' << _a2 << endl;}private:int _a1;int _a2;
};int main()
{A a;//实参不能显示传递 this 指针//a.Print(&a)(x)a.Print();return 0;
}
4 用 C++ 实现 Stack
接下来我们用 C++ 来实现栈 Stack,来对比与 C语言实现栈的异同。
C++:
#include<iostream>
#include<assert.h>using namespace std;class Stack
{
public://初始化void Init(int n = 4){_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc fail!\n");return;}_top = 0;_capacity = n;}//入栈void Push(int x){//满了先扩容if (_top == _capacity){int newCapacity = 2 * _capacity;int* tmp = (int*)realloc(_arr, sizeof(int) * newCapacity);if (tmp == nullptr){perror("realloc fail!\n");return;}_arr = tmp;_capacity = newCapacity;}_arr[_top++] = x;}//出栈void Pop(){assert(_top > 0);_top--;}//返回元素个数size_t Size(){return _top;}//返回栈顶元素int Top(){assert(_top > 0);return _arr[_top - 1];}//判空bool Empty(){return _top == 0;}//销毁void Destroy(){free(_arr);_arr = nullptr;_top = _capacity = 0;}private:int* _arr;size_t _top;size_t _capacity;
};int main()
{//用 Stack 实例化对象 stStack st;st.Init();st.Push(1);st.Push(2);st.Push(3);st.Push(4);while (!st.Empty()){cout << st.Top() << ' ';st.Pop();}return 0;
}
运行结果:
输出:
4 3 2 1
C语言:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int STDataType;typedef struct Stack
{STDataType* _arr;size_t _top;size_t _capacity;
}ST;//初始化
void STInit(ST* ps)
{ps->_arr = NULL;ps->_top = ps->_capacity = 0;
}//入栈
void STPush(ST* ps, int x)
{//满了先扩容if (ps->_top == ps->_capacity){int newCapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;int* tmp = (int*)realloc(ps->_arr, sizeof(STDataType) * newCapacity);if (tmp == NULL){perror("realloc fail!\n");return;}ps->_arr = tmp;ps->_capacity = newCapacity;}ps->_arr[ps->_top++] = x;
}//出栈
void STPop(ST* ps)
{assert(ps->_top > 0);ps->_top--;
}//返回元素个数
size_t STSize(ST* ps)
{return ps->_top;
}//返回栈顶元素
STDataType STTop(ST* ps)
{assert(ps->_top > 0);return ps->_arr[ps->_top - 1];
}//判空
bool STEmpty(ST* ps)
{return ps->_top == 0;
}//销毁
void STDestroy(ST* ps)
{free(ps->_arr);ps->_arr = nullptr;ps->_top = ps->_capacity = 0;
}int main()
{ST st;STInit(&st);STPush(&st, 1);STPush(&st, 2);STPush(&st, 3);STPush(&st, 4);while (!STEmpty(&st)){printf("%d ", STTop(&st));STPop(&st);}return 0;
}
运行结果:
输出:
4 3 2 1
上面写出了 C++ 版本的 Stack 的实现与 C 语言版本的 Stack 的实现,通过对比,发现 C++ 的接口使用起来是更加简单的,不需要传递 st 的地址,而且 C 语言的 Stack 并没有将成员变量和成员函数封装在一起,C++ 将成员变量和成员函数封装在了一起,隐藏起了实现细节,所以使用起来是更加方便的。