您现在的位置是:首页 >其他 >从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针网站首页其他
从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针
目录
前面我们提到C++是面向对象的语言,但不是纯面向对象,因为要兼容C语言,
所以C++可以面向对象和面向过程混编,像Java是纯面向对象的语言,只有面向对象,
就算你想实现一个排序也要写一个类出来……
本章将正式开始学习C++中的面向对象。
1. 面向对象
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
举个栗子,比如设计简单的外卖系统 ~
面向过程:关注实现下单、接单、送餐这些过程。体现到代码层面 -- 方法/函数
面向对象:关注实现类对象及类对象间的关系,用户、商家、骑手以及他们之间的关系。
体现到代码层面 —— 类的设计及类之间的关系。
1.1 类的引入
在C语言中,结构体中只能定义变量……
而在C++中,结构体内不仅可以定义变量,还可以定义函数!
因为在 C++ 里,struct 也跟着升级成了类。
我们可以使用 struct 或 class 来定义类,它们之间有什么区别我们后面再说。
因为 C++ 兼容 C 里面结构体的用法。
所以 C++ 就可以直接使用类名来定义了:
struct Student
{
char name[10];
int age;
int id;
};
int main()
{
struct Student s1; // 兼容C
Student s2; // C++就可以直接使用类名,Student类名,也是类型。
strcpy(s1.name, "小明");
s1.id = 10001;
s2.age = 20;
strcpy(s2.name, "小红");
s2.id = 10002;
s2.age = 19;
return 0;
}
我们既能用 struct Student s1 来定义,还能直接使用 Student s2,通过使用类名直接定义。
这体现了 C++ 兼容 C 的特点。
但是如果这是在 C语言 里, stuct Student 才是它的类型,
直接使用 Student 定义是不可以的(当然,用 typedef 就另当别论了)。
它其实就是一个结构,你可以理解成和我们之前学的结构体是 "一样的" ,只是定义的方式既兼容了 C 还兼容了 C++ ,但是下面我们还会认识到一些它的不同之处,我们继续往下看。
如果是在C语言里,结构体里只能定义变量,就是一个多个变量的集合。
如果我们想要将 s1 中的变量进行初始化,我们还得一个个写,真的是很麻烦:
但是在C++里,我们不仅可以定义变量,还可以定义函数(方法)。
我们可以定义一个 "初始化" 函数:
struct Student
{
/* 成员变量 */
char name[10];
int age;
int id;
/* 成员方法 */
void Init(const char* name, int age, int id) {
...
}
};
我们在 C++ 中一般称这些变量为成员变量,称这些函数为成员方法。
这个时候似乎发现了一些新的问题!
我们的这个成员方法的参数名取的好像和成员变量里一样了,
比如我们访问 name 的时候到底是成员变量里的 name 还是成员方法里的 name 呢?
这就让人区分不开了……
为了能够更好的区分哪个是成员变量,我们在定义成员变量名时可以给它们做一些标记:
下面是几种常见的表示为成员变量的 "风格" :
① 前面加斜杠 :
char _name[10];
② 后面加斜杠:
char name_[10]
③ 前面加个 m (表示成员 member):
char mname[10]
这个并没有明确的规定,不同的公司也有不同的风格。我们这里用第一种风格
这样就可以区分开来了:
struct Student
{
/* 成员变量 */
char _name[10];
int _age;
int _id;
/* 成员函数 */
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
};
为了方便测试,我们再来写一个简单的打印函数,调用它们进行一个打印: :
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
struct Student
{
/* 成员变量 */
char _name[10];
int _age;
int _id;
/* 成员函数 */
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << " " << _age << " " << _id << endl;
}
};
int main()
{
struct Student s1;
Student s2;
/* 初始化 */
s1.Init("小明", 20, 10001);
s2.Init("小红", 19, 10002);
/* 打印 */
s1.Print();
s2.Print();
return 0;
}
总结:C++ 对我们的 struct 进行升级了,升级为类了。它兼容以前的用法,又有了新的用法。
1.2 class 关键字
我们刚才引入部分讲了 struct ,我们知道了它在 C++ 里升级成了类。
其实 C++ 也有自己的亲儿子,就是 class
class语法和struct一样,注意类定义结束时后面要加分号!
但我们把上面代码的struct改成class居然报错了,这又是为什么呢?
因为 C++ 讲究 "封装" ……C++ 这里是它把数据和方法都放在了类里面。
这和C语言是不同的,C语言里数据是数据,方法是方法。
这里我们就来提一下 面向对象的三大特性:封装、继承、多态。
我们先来重点看一下这个 封装 :
① 数据和方法都被放在了一起。
② 访问限定符
就是因为这个访问限定符,所以这里我们报错了,我们下面来学习一下访问限定符。
2. 类的访问限定符及封装
2.1 访问限定符
C++ 实现封装的方式:用类将对象的属性与方法结合在一起,让对象更加完善,
通过访问权限选择性地将其接口提供给外部的用户使用。
一共有三种访问限定符,分别是 public(公有)、protected(保护)、private(私有)。
这一听名字就能知道,公有就是随便访问,保护和私有就是不让你随便访问得到。
访问限定符说明
① public 修饰的成员,可以在类外面随便访问(直接访问)。
② protected 和 private 修饰的成员,不能在类外随便访问。
(此处 protected 和 private 是类似的,现在你可以认为他们是一样的,后面我们讲继承的时候才能体现出它们两的区别)
这就分出了两个阵营,一个阵营是可以随便访问的,一个阵营是不能随便访问的。
③ class 的默认访问权限为 private,struct 为 public !
这就是为什么我们刚才编译会报错,因为 class 默认访问权限是 private!
那好,既然知道问题所在了,我们该如何解决让它成功访问呢?
使用访问限定符就可以了!
使用我们的访问限定符,加一个 public :
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Student
{
/* 成员变量 */
char _name[10];
int _age;
int _id;
public:
/* 成员函数 */
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << " " << _age << " " << _id << endl;
}
};
int main()
{
struct Student s1;
Student s2;
/* 初始化 */
s1.Init("小明", 20, 10001);
s2.Init("小红", 19, 10002);
/* 打印 */
s1.Print();
s2.Print();
return 0;
}
搞定!我们再来细说一下刚才加进去的访问限定符。
③ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
④ 如果后面没有访问限定符,作用域就到 { (最外面花括号)类结束。
也就是说,我们刚才加进去的 public ,
从它开始到下一个访问限定符出现为止的这块范围,都是共有的了,
但是因为后面没有再次出现访问限定符,所以作用域就到类结束为止。
我们再加一个访问限定符 private 进去看看:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Student
{
/* 成员变量 */
char _name[10];
int _age;
int _id;
public:
/* 成员函数 */
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
private:
void Print()
{
cout << _name << " " << _age << " " << _id << endl;
}
};
int main()
{
struct Student s1;
Student s2;
/* 初始化 */
s1.Init("小明", 20, 10001);
s2.Init("小红", 19, 10002);
/* 打印 */
s1.Print();
s2.Print();
return 0;
}
现在, public 能影响到的范围就到 private 出现前为止了,然后在main里的打印函数就会报错。
这就是访问限定符在这里起到的一个作用。
注意事项:
① 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
② 我们一般在定义类的时候,建议明确定义访问限定符,
不要用 struct / class 的默认的访问权限,就像这样:
class Student
{
private:
char _name[10];
int _age;
int _id;
public:
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << " " << _age << " " << _id << endl;
}
};
虽然你不指定他会有默认限定,但是还是建议你明确写出来,
因为这样能让人一眼就看出它是共有的还是私有的。
2.2 封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,
仅对外公开接口来实现对象进行交互。
① 把数据都封装到类里面。
② 可以给你访问定义成公有,不想给你访问的定义成私有或者保护。2.2.1封装的意义
封装的意义是什么?
封装是一种更好的严格管理,不封装是一种自由管理。
那么是严格管理好,还是自由管理好呢?
举一个疫情防控的例子:
某国单日新增一百万,你说是自由的管理好呢?还是严格的管理好呢?
我们和某国其实都是在控制疫情的,但是我们是严格的管理,控制疫情。
某国是自由管理,虽然人人高呼 "Freedom" ,但是疫情持续了很长时间。
2.2.2 封装的本质
类也是一样,我们使用类数据和方法都封装到了一起,不想让人随意来访的,
就是用 protected / private 把成员封装起来,开放一些共有的成员函数对成员合理的访问。
C语言没办法管理,易出错,全靠素质。
所以,封装是一种更好、更严格的管理!
3. 类的作用域和实例化
3.1 类定义的两种方式
① 声明和定义全部放在类中
class Student
{
public:
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Print()
{
cout << _name << " " << _age << " " << _id << endl;
}
private:
char _name[10];
int _age;
int _id;
};
注意事项:
成员函数如果在类中定义,编译器可能会将它当作内联函数来处理。
注意,是可能。并不是说一定会成为内联函数,之前讲内联函数的时候我们也说了。
内联函数对编译器来说也只是一个建议。至于到底会不会成为内联是由编译器来决定的。
取决于编译器看这个函数符不符合条件,一般一个函数太长(大概是十行左右),
或者函数是一个递归,编译器就不会让它成为内联了。
② 声明和定义分离:
比如创建一个学生类
Student.h:
class Student
{
public:
void Init(const char* name, int age, int id);
void Print();
private:
char _name[10];
int _age;
int _id;
};
Student.cpp:
#include "Student.h"
void Student::Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void Student::Print()
{
cout << _name << " " << _age << " " << _id << endl;
}
这函数名前的 : : 是什么?我们继续往下看
3.2 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类外定义成员,需要使用作用域解析符 : : 来指明成员属于哪个类域。
class Student
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Student::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
注意到这样写不会报错
class Student
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
void PrintPersonInfo()//不会和上面的函数冲突,因为两者属于不同的域
{
//cout << _name << " " << _gender << " " << _age << endl;
//没注释掉就是未定义的标识符
}
我们再看下函数重载的定义:
函数重载:C++ 允许在同一个作用域中存在同名的函数。
下面三个不同只要满足一个不同,就可以触发函数重载:
① 参数类型不同
② 参数个数不同
③ 参数顺序不同
所以这里不报错也不会发生函数重载
还有像前面说过比如说我们写一个比较正式一点的项目(声明和定义分离)
Stack.h:
class Stack
{
public:
void Init();
void Push(int x);
// ...
private:
int* _array;
int _top;
int _capacity;
};
Stack.cpp:
#include "Stack.h"
// 这里需要指定 Init 是属于 Stack 这个类域
// 我们使用 :: 来指定
void Stack::Init()
{
_array = nullptr;
_top = _capacity = 0;
}
这里我们平常练习为了方便使用就不使用声明和定义分离的方式了
3.3 类的实例化
首先要说清楚的是:类本身是没有存储空间的。
通过类建立出对象,即实例化,才会有实际的存储空间。
我们把用类类型创建对象的过程称为 —— 类的实例化。
① 类只是一个像 "图纸" 一样的东西,限定了类有哪些成员。定义出一个类,
并没有分配实际的内存空间来存储它。
② 一个类可以实例化出多个对象,占用实际的物理空间,存储类成员的变量。
举个例子:
类实例化对象就像是在现实中使用设计图建造房子,类就像是设计图。
你可以根据这个设计图造出很多栋楼出来。
只有楼建好了里面才能住人,你才能放家具、放东西进去。
设计图能住人吗?当然是不能!因为并没有分配实际的内存空间。
只有你照着设计图去建造,即实例化出对象,占用实际的物理空间,才能住人。
3.3.1 声明和定义的区别
函数的声明和定义直接看有没有花括号就行了,但变量呢?
对于变量而言,声明和定义的区别就是有没有开辟空间
对于变量而言如果要声明和定义分离在.h文件要+extern关键字变成声明(或者+static)
class Stack
{
public:
void Init();//没开空间,是声明
void Push(int x);//没开空间,是声明
// ...
private:
int* _array;//没开空间,是声明
int _top;//没开空间,是声明
int _capacity;//没开空间,是声明
};
int capacity;//开空间了,是定义
void Stack::Init() //开空间了,是定义
{
_array = nullptr;
_top = _capacity = 0;
}
4. 类对象模型
4.1 计算类的存储大小
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?
我们该如何计算一个类的大小呢?
比如这个栈和它定义出来的对象是多大呢?
#include <iostream>
using namespace std;
class Stack
{
public:
void Init();
void Push(int x);
private:
int* _array;
int _top;
int _capacity;
};
void Stack::Init()
{
_array = nullptr;
_top = _capacity = 0;
}
int main()
{
Stack s;
s.Init();
cout << sizeof(Stack) << endl;
cout << sizeof(s) << endl;
return 0;
}
运行结果如下:(32位环境)
对象中存了成员变量,没存成员函数。计算类或类对象的大小只看成员
C++成员函数存放在公共的代码段,并且要考虑内存对齐,C++内存对齐规则和C结构体一致。
对内存规则不熟的可以回去复习下:C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)_GR C的博客-CSDN博客
看一段代码,下面类的大小是多少呢?
// 类中既有成员变量,又有成员函数
class A1
{
public:
void f1() {}
private:
char _c;//1
int _a;//4 (根据内存对齐,浪费前面3字节,计算出8字节)
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有 - 空类
class A3
{};
先看结果:
A2 没有成员变量,A3 更是什么都没有,为什么大小是 1 呢?为什么不是 0 呢?
我们尝试给创建出的对象取地址,它们是有地址的,
// 类中既有成员变量,又有成员函数
class A1
{
public:
void f1() {}
private:
char _c;//1
int _a;//4 (根据内存对齐,浪费前面3字节,计算出8字节)
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有 - 空类
class A3
{};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
A1 a1;
A2 a2;
A3 a3;
cout << &a1 << endl;
cout << &a2 << endl;
cout << &a3 << endl;
return 0;
}
取地址就是要拿出它存储空间的那块,所以这里总不能给一个空指针吧?
如果大小给 0 的话就没办法区分空间了。
所以,空类会给 1 字节,这 1 字节不存储有效数据,只是为了占个位,表示对象存在。
5. this指针
5.1 引出 this 指针
我们首先来定义一个日期类 Date:
#include <iostream>
using namespace std;
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;
d1.Init(2023, 4, 23);
d1.Print();
Date d2;
d2.Init(2023, 5, 1);
d2.Print();
return 0;
}
Date 类中有 Init 和 Print 两个成员函数,函数体中没有关于不同对象的区分,
那当 d2 调用 Print 函数时,这个 Print 函数是如何知道要打印 d2 对象的?
而不是去打印 d1 对象呢?
看看反汇编,call Print的地址一样,说明调用的是同一个函数
因为C++ 通过引入了隐藏的 this 指针解决该问题。
C++ 编译器给每个 "非静态的成员函数" 增加了一个隐藏的指针参数,
让该指针指向当前对象(函数运行时调用该函数的对象),它是系统自动生成的,
在函数体中所有成员变量的操作,都是通过该指针去访问。
只不过所有的操作对程序员来说是透明的,
就是不需要程序员自己来传递,编译器自动帮你去完成。
上面的函数传参和定义会被改成这样:( Init 第一个参数也会加上 this 指针)
打印的代码也会变成这样:(可以自己加 this ->也可以不加,上面的就不能加)
cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
5.2 this 指针的使用和特性
① 调用成员函数时,不能 "显示地" 传实参给 this :
② 定义成员函数时,也不能 "显示地" 声明形参 this :
③ 但是,在成员函数内部,我们可以 "显示地" 使用 this :(同上图)
也就是说,你不写 this 他会自动加,你写了他也是允许你写的。
然可以 "显示地" 用,但是一般情况下我们都不会自己 "显示地" 写 。
this 指针还被 const 修饰,所以 this 指针是不能修改的。
this 指针的本质是一个常量指针,是通过 const 修饰 this 指针指向的内存空间。
6. 笔试选择题
6.1 结构体大小
有一个如下的结构体:
struct A
{
long a1;
short a2;
int a3;
int *a4;
};
请问在64位编译器下用sizeof(struct A)计算出的大小是多少?( )
A.24
B.28
C.16
D.18
6.2 在C++中的结构体是否可以有成员函数?( )
A.不可以,结构类型不支持成员函数
B.可以有
C.不可以,只有类允许有成员函数
6.3 关于this指针使用方法的叙述正确的是( )
A.保证基类保护成员在子类中可以被访问
B.保证基类私有成员在子类中可以被访问
C.保证基类公有成员在子类中可以被访问
D.保证每个对象拥有自己的数据成员,但共享处理这些数据的代码
6.4 关于this指针描述错误的是( )
A.this指针是非静态成员函数的隐含形参.
B.每个非静态的成员函数都有一个this指针.
C.this指针是存在对象里面的.
D.this指针可以为空
6.5 下面程序编译运行结果是?
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
6.6 下面程序编译运行结果是?
#include <iostream>
using namespace std;
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
答案
6.1 A
64位系统下指针为8个字节,a1占4字节,a2两字节,由于a3占4字节, a2需要补齐2个字节,对于a1,a2,a3一共开辟了12个字节,由于a4占8个字节,所以a4之后要 补齐4个字节才能是8的整数倍,最后总和为24字节,刚好也是8的倍数,所以最终结构体大小为24字节
6.2 B
A.C语言结构体不支持成员函数,但C++结构体支持,其class与struct本质没有区别,唯一区别在于默认时class的访问属性为私有,struct为公有
B.正确
C.C++结构体也支持成员函数
6.3 D
A.基类保护成员在子类可以直接被访问,跟this无关
B.基类私有成员在子类中不能被访问,跟this无关
C.基类共有成员在子类和对象外都可以直接访问,跟this无关
D.this指针代表了当前对象,能够区分每个对象的自身数据,故正确
6.4 C
A.静态成员函数没有this指针,只有非静态成员函数才有,且为隐藏指针
B.非静态成员函数的第一个参数就是隐藏的this指针
C.this指针在非静态的成员函数里面,对象不存在,故错误
D.单纯的对this赋空是不可以的,不过可以强转直接赋空,不过一般不进行这样的操作
6.5 C、正常运行
#include <iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
//把p的地址传给Print this指针就是nullptr,上面没有对空指针解引用,所以正常运行
return 0;
}
6.6 B、运行崩溃
#include <iostream>
using namespace std;
class A
{
public:
void PrintA()
{
cout << _a << endl;//会被改为 cout << this->_a << end;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();//同上题,但上面对空指针解引用了
return 0;
}
本篇完。