一.类的定义与对象的创建
(一).类
I.定义
简单来说,类是相较于C语言中的“结构体“”而言更为全面的数据类型声明,并不占用内存空间,只有在“类的实例化(通过类名创建对象)”过程中才会需要内存空间,其区别在于结构体中仅包括“成员变量”(基本类型变量或者是结构体类型变量),而类中不仅可以包含“成员变量”,也可以包含对成员变量进行操作的“函数”
实例如下:
//C语言中的结构体
struct Student{
//成员变量
int number;// 排名
char *name;//学生姓名
int grate; //学生成绩
};
//输出函数
void Printf(struct Student ud){
printf("排名为%d的同学是%s,他的成绩是%d",ud.number,ud.name,ud.grate);
}
int main(){
struct Student ud; //定义一个类型struct Student变量ud
ud.number=3; //成员变量的初始化
ud.name="小明";
ud.grate=96;
Printf(ud); //函数调用
}
II.类结构定义
关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表
III.类成员访问方式
方法一:通过“ . ”访问
创建对象后,可以通过“ . ”来进行访问成员
实例如下:
int main(){
//通过类来定义变量,即创建对象
class Student stu1; //也可以省略关键字class
//为类的成员变量赋值
ud.number = 3;
ud.name = "小明";
ud.score = 96;
//调用类的成员函数
ud.Print();
return 0;
}
方法二:通过“->”(对象指针)访问
对象指针:就是指向对象的一个指针,通过指针对成员进行操作
实例如下:
(1)在栈(Stack)上创建对象
int main(){
//通过类来定义变量,即创建对象
class Student stu1; //也可以省略关键字class
class Student *ud=&stu1;
//为类的成员变量赋值
ud->number = 3;
ud->name = "小明";
ud->score = 96;
//调用类的成员函数
ud->Print();
return 0;
}
(2)在堆(Heap)上创建变量
int main(){
//通过类来定义变量,即创建对象
class Student *ud=new Student;
//为类的成员变量赋值
ud->number = 3;
ud->name = "小明";
ud->score = 96;
//调用类的成员函数
ud->Print();
delete ud; //一定要记得删除,释放空间,防止堆积
return 0;
}
(3)通过引用符来操作
using namespace std;
class Studnet{
public:
int age;
int grate;
};
int main(){
Student td;
Student &r=td;
r.age=12;
r.grate=90;
return 0;
}
(4)堆和栈存储区别
I.在栈上创建的对象是有名字的,不用指针指向也可以找到它;但是在堆上创建的对象是没有名字的,我们需要通过指针来指向它,才能访问其成员变量和成员函数
II.栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;堆内存由程序员管理,对象使用完毕后可以通过 delete 删除.
IV.类成员访问范围
(1)在类的成员函数内部,能够访问:
-当前对象的全部属性、函数;
-同类其他对象的全部属性、函数
(2)在类的成员函数以外的地方,只能够访问该类对象的公有成员
(二).对象(object)
在C语言中可以通过结构体名来定义变量,在 C++ 中可以通过类名来定义变量。不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象(Object)。
实际例子如下:
//通过class关键字类定义类
class Student{
public:
//类包含的变量
char *name; //学生姓名
int number; //学生名次
int score; //学生成绩
//类包含的函数
void Print(){
printf("排名为%d的同学是%s,他的成绩是%d",number,name,grate);
}
};
int main(){
//通过类来定义变量,即创建对象
class Student stu1; //也可以省略关键字class
//为类的成员变量赋值
ud.number = 3;
ud.name = "小明";
ud.score = 96;
//调用类的成员函数
ud.Print();
return 0;
}
在 C++ 中,通过类名就可以创建对象,这个过程叫做类的实例化,因此也称对象是类的一个实例
(Instance)。
有些资料也将类的成员变量称为属性(Property),将类的成员函数称为方法(Method),
在面向对象的编程语言中,经常把函数(Function)称为方法(Method)。
(三).数据类型图(回顾)
八大基本数据结构:https://www.php.cn/java/base/479128.html
(四).拓展(new与malloc用法)
I.new用法
(1)结构
//创建
(数据类型)*(变量名)= new (数据类型)[变量个数];
//销毁
delete[]变量名;
(2)具体使用演示
//例1
int *a=new int ;//申请单个变量空间
delete a; //销毁
//例2
int *a=new int[5];//申请多个连续的变量空间
delete[]a;//销毁
II.malloc的用法
(1)结构
//创建
(数据类型)*(变量名)=(数据类型 *)malloc(sizeof(数据类型)*(变量个数))
//销毁
free(变量名);
(2)具体使用演示
int *a = (int *)malloc(sizeof(int)*5);//创建
free(a); //释放
注意:new和delete配套使用,malloc与free配套使用
二.类的成员变量与成员函数
(一)定义
类简单来说为一种抽象的数据类型,普通数据类型仅包含“成员变量”,而类中既包含“成员变量”又包含“成员函数”
(1)类的成员变量和普通变量一样,也有数据类型和名称,占用固定长度的内存,定义类的时候不能对成员变量赋值,类是一种数据类型定义,不占用内存空间,只有在用类定义对象时才会占用空间
(2)类的成员函数也和普通函数一样,都有返回值和参数列表,它与一般函数的区别是:成员函数是一个类的成员,出现在类体中,成员函数的作用范围由类来决定;而普通函数是独立的,作用范围是全局的或在某个命名空间内。
疑问:成员函数占用内存空间吗?
成员函数也是函数,函数都是有地址的,所以函数是占用内存空间的,但函数由于是多对象共享的,所以函数不占用对象的内存地址,是放在代码段的
(二)成员函数
如果函数是在类中被定义,则为内联函数,反之,则不是
//方式1
using namespace std;
class Student{
public:
int number;
char *name;
int score;
public:
void Print();//函数声明
}
//函数定义
viod Student::Print(){
cout<<name<<"的名次为"<<number<<"成绩为"<<score<<endl;}
//方式2
using namespace std;
class Student{
public:
int number;
char *name;
int score;
public:
//内联函数
void Print(){
cout<<name<<"的名次为"<<number<<"成绩为"<<score<<endl;}
};
// 方式3(通过inline将外部定义函数变为内敛函数)
using namespace std;
class Student{
public:
int number;
char *name;
int score;
public:
void Print();//函数声明
}
//函数定义
inline viod Student::Print(){
cout<<name<<"的名次为"<<number<<"成绩为"<<score<<endl;}
"::"域操作符,连接类名和函数名
推荐用方式1(类外部定义,内部声明),是一个好习惯
注意:成员函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前。
(三)构造函数和析构函数
I.构造(Constructor)函数
定义:
成员函数的一种,名字和类名相同,可以有参数,不能有返回值(void也是不可以的)
作用
对对象进行初始化
如果定义类时没有写构造函数,编译器会自动生成一个默认的无参构造函数,不做任何操作,
在定义对象时构造函数被自动调用,对象生成后,不能在使用构造函数
注意:一个类可以有多个构造函数(参数类型或个数不同)
分类:
复制构造函数和类型构造函数
应用实例:
using namespace std;
class Complex { private:
double real, imag;
public:
//自定义的构造函数,要传入相应的参数才可以使用,i为可缺省参数
Complex(double r, double i = 0) {
cout << "初始化成功!";
};
};
int main() {
Complex c1(1);
return 0;
}
复制构造(copy constructor)函数
定义:
只有一个参数,即对同类对象的引用的构造函数
形式:
X::X(X&) 或 X::X(const X&)
如果未定义复制构造函数,则编译器生成默认的复制构造函数,如果已经自定义,则默认的便不存在
应用场景
场景一:当用一个对象去初始化另一个对象时
using namespace std;
class Student{
public:
int age;
int grate;
};
int main(){
Student td1;
Student td2(td1);//调用默认的复制构造函数
Studnet td3=td2;//初始化语句,非赋值语句
return 0;
}
场景二:如果某函数参数是类A的对象,该函数被调用时,类A的复制构造函数将被调用
using namespace std;
class Student {
public:
int grate;
int age;
public:
//构造函数
Student() {
cout << "Hello World!" << endl;
};
//复制构造函数
Student(Student& td) {
cout << "You're welcome!" << endl;
};
};
void Func(Student td) {
cout << "You are right!" << endl;
}
int main() {
Student td;//调用构造函数
Func(td);//调用复制构造函数
return 0;
}
运行结果:
场景三:如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调
using namespace std;
class Student {
public:
int age;
public:
Student(int n) { age = n; };
Student(const Student& td) {
age = td.age;
cout << "You are welcome!" << endl;
};
};
Student Func() {
Student td(5);
return td;
}
int main() {
cout << Func().age;
return 0;
}
运行结果如下:
注意:对象间赋值并不导致复制构造函数被调用
类型转换构造函数
定义:
只有一个参数,而且不是复制构造函数,一般就可以看作是转换构造函数
目的:
实现类型的自动转换
应用实例:
using namespace std;
class Student {
public:
int age;//学生年龄
int grate;//学生分数
public:
//类型转换构造函数
Student(int i) {
cout << "You're welcome!" << endl;
age = i, grate = 0;
}
Student(int a, int b) {
age = a, grate = b;
}
};
int main() {
Student td(3, 56);
Student ud = 12;//类似于ud=Student(12);
td = 23;//23被自动转换为一个临时的Student对象
cout << td.age << " " << td.grate << endl;
return 0;
}
显式类型装换构造函数:
在类型转换函数前加关键字explicit,便无法直接通过整数来自动转换为临时对象,必须通过构造函数格式
注意:在对象间赋值时不会调用类型转换构造函数
运行结果:
II.析构(Destructors)函数
定义:
名字和类名相同,在前面加‘~’,没有参数和返回值,一个类最多只能有一个析构函数
析构函数对象消亡时自动被调用,定义析构函数可用来做善后工作,比如释放分配的空间
如果未定义析构函数,编译器自动生成无参的析构函数,若已定义,编译器便不再生成
代码演示如下:
using namespace std;
class Student{
public:
int age;
public:
~Student(){
cout<<"You're welcome!"<<endl;
}
};
int main(){
Student td[3];
return 0;
}
运行结果:
构造函数和析构函数实例:
using namespace std;
class Student {
public:
int age;
//构造函数
Student() { age = 9; };
//复制构造函数
Student(Student& td) {
cout << "You're welcome!\n";
}
//析构函数
~Student() {
cout << "Hello\n";
}
};
int main() {
Student td;
Student ud(td);
Student id[3];
return 0;
}
运行结果:
函数重载
构造函数之间的参数类型或参数个数不同
using namespace std;
class Student {
public:
int grate;
int age;
public:
//构造函数1
Student(int s1, int s2) {
grate = s1, age = s2;
};
//构造函数2
Student(int s3) {
grate = s3,age=0;
};
//构造函数3
Student() {
grate = 54, age = 23;
};
public:
//输出对象成员
void Print() {
cout << "grate=" << grate << " " << "age=" << age<<endl;
};
};
int main()
{
Student td1(2,3);//构造函数1初始化
td1.Print();
Student td2(2); //构造函数2初始化
td2.Print();
Student td3; //构造函数3初始化
td3.Print();
return 0;
}
三.类成员的访问权限及类的封装
(一)类成员的访问限制
- public:类的对象、友元、子类均能访问的成员。
- protected:仅有子类能够通过类的成员函数方式进行访问,对于子类的对象也不可访问。对于本类的对象以及友元函数均不可访问。
- private:仅本类的成员以及有缘可以访问,对于该类的对象不可访问。
- “隐藏”,是设置私有成员的一种机制,目的是强制对成员变量的访问一定要通过成员函数进行,如果成员变量的属性更改后便于通过成员函数修改
- 以上三种关键字出现的次数和先后次序没有限制没有上述关键字,则缺省地被认为是私有成员
如果某个成员前面
I.private和public访问权限问题
using namespace std;
//类的声明
class Student{
private: //私有的
char *m_name;
int m_age;
float m_score;
public: //共有的
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
};
//成员函数的定义
void Student::setname(char *name){
m_name = name;
}
void Student::setage(int age){
m_age = age;
}
void Student::setscore(float score){
m_score = score;
}
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
int main(){
//在栈上创建对象
Student stu;
stu.setname("小明");
stu.setage(15);
stu.setscore(92.5f);
stu.show();
//在堆上创建对象
Student *pstu = new Student;
pstu -> setname("李华");
pstu -> setage(16);
pstu -> setscore(96);
pstu -> show();
return 0;
}
private下的成员变量不能通过类定义的对象进行赋值,可以通过类内部
public下的成员函数来实现赋值
成员变量大多是以m_开头,是约定俗成的,为了和成员函数中的形参名字区分开
一般的,我们可以通过set()函数对privite下的成员变量进行赋值,通过get()函数读取成员变量的值
II.protected访问权限问题
只有在派生类中,派生类的成员变量才可以访问基类的成员变量
代码如下:
//基类
class Base
{
public:
Base();
private:
void foo();
protected:
int m;
};
//派生类
class Derive : public Base
{
public:
Derive();
};
Derive::Derive()
{
this->m = 1; // 这里访问m没有问题
}
(二).类的封装
I.定义
我们可以把程序按某种规则分成很多“块”,类与类之间可能会有联系,每个类都有一个可变部分(public)和一个不可变部分(private)。我们需要把可变部分和不可变部分分离出来,将不可变的部分暴露给其他类,而将可变的部分隐藏起来,以便于随时可以让它修改。这项工作就是封装。
通俗理解:每个人都有自己的不想让别人知道的小秘密,类也有,为了对这些内容进行限制,我们可以通过public和private来实现这个功能
private 关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为 public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为 private。
根据C++软件设计规范,实际项目开发中的成员变量以及只在类内部使用的成员函数(只被成员函数调用的成员函数)都建议声明为 private,而只将允许通过对象调用的成员函数声明为 public。
声明为 private 的成员和声明为 public 的成员的次序任意,既可以先出现 private 部分,也可以先出现 public 部分。如果既不写 private 也不写 public,就默认为 private
补充:
I.引用操作符(“&”)的应用
通俗理解为类似于一个别名,某个变量的引用,相当于给该变量起了个别名,如果使用别名,与直接使用该变量是一样的,如果通过别名来改变其值也是可以的。
格式:
类型名 &引用名=变量名
要求:
(1)定义引用时一定要初始化某个变量
(2)初始化后,就不能再引用其他变量(从一而终)
(3)只能引用变量,不能引用常量和表达式
注意:如果引用被常变量定义,则不可以通过引用来改变变量的值
操作实例:
//引用的操作实例之“交换两个数”(通过指针不是很美观)
using namespace std;
class Swap {
public:
int n1, n2;
public:
void print(int& n1, int& n2 ) {
int temp;
temp = n1;
n1 = n2;
n2 = temp;
}
};
int main() {
Swap st;
int m = 9;
const int n=0;
m = n;
st.n1 = 10;
st.n2 = 11;
st.print(st.n1, st.n2);
cout << "n1的值为:" << st.n1<<endl;
cout << "n2的值为:" << st.n2;
return 0;
}
运行结果为:
II.const关键字用法解析
用法一:定义常量
using namesapce std;
int main(){
const int a=9;
}
用法二:定义常量指针
(1)不可以通过常量指针修改其指向内容
using namespace std;
int main(){
int n,m=9;
int*p=&m;
*p=67;//错误用法
return 0;
}
(2)不能把常量指针赋值给非常量指针
int a=0,b=7;
const int *p=&a;
int *q=b;
p=q;//正确
q=p;//错误用法
q=(int*)p;//通过强制类型转化也可以实现
(3)指针参数为常量指针时,可以避免内部不小心修改指针所指向内容
用法三:定义常引用
注意:不能通过常引用修改其值