目录
1. 面向过程和面向对象
2. 类的引入
3. 类的定义
4. 类的访问限定符及封装
4.1 访问限定符
4.2 封装
5. 类的作用域
6. 类的实例化
7. 类对象模型
7.1 如何计算类对象的大小
7.2 类对象的存储方式猜测
7.3 结构体内存对齐规则
8. this指针
8.1 this指针的引出
8.2 this指针的特性
8.3 this指针存在哪里?
8.4 判断下列代码理解this指针
1. 面向过程和面向对象
C语言:是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++:是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
- 什么是对象?
对象是一个实体,我们眼睛看到的所有实体都可以看成一个实体对象。- 什么是类?
类是用来对实体(对象)进行描述的。(对象有什么属性,有什么功能)类是一种自定义类型。
2. 类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数。
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const DataType& data)
{
// 扩容
_array[_size] = data;
++_size;
}
DataType Top()
{
return _array[_size - 1]; }
void Destroy()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s;
s.Init(10);
s.Push(1);
s.Push(2);
s.Push(3);
cout << s.Top() << endl;
s.Destroy();
return 0;
}
上面结构体的定义, 在C++中更喜欢用class来代替。
3. 类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{ } 中为类的主体,注意类定义结束时后面分号不能省略。
- 类体中内容称为类的成员;
- 类中的变量称为类的属性或成员变量;
- 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名 ::
一般情况下,更期望采用第二种方式。
注意:上课为了方便演示使用方式一定义类,大家后序工作中尽量使用第二种。
成员变量命名规则:
class Date
{
public:
void Init(int year,int mouth,int day)
{
_year = year;
_mouth = mouth;
_day = day;
}
void print()
{
cout << _year << "-" << _mouth << "-" << _day << endl;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
d1.Init(2024,3,28);
d1.print();
return 0;
}
或者声明和定义分离:
// data.h文件
class Date
{
public:
void Init(int year,int mouth,int day);
void print();
private:
int _year;
int _mouth;
int _day;
};
// data.cpp文件
void Date :: Init(int year,int mouth,int day)
{
_year = year;
_mouth = mouth;
_day = day;
}
void Date :: print()
{
cout << _year << "-" << _mouth << "-" << _day << endl;
}
// test.cpp文件
int main()
{
Date d1;
d1.Init(2024,3,28);
d1.print();
return 0;
}
4. 类的访问限定符及封装
4.1 访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用。
【访问限定符说明】
1. public修饰的成员在类外可以直接被访问。
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别 。
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
注意:在继承和模板参数列表位置, struct和class也有区别,后序给大家介绍。
4.2 封装
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法放到类里面进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装的意义:
(1)保护类中的成员,不让类以外的程序直接访问或修改,只能通过提供的公共接口访问(数据封装)。
(2)隐藏方法,只要接口不变,内容的修改不会影响到外部的调用者(方法封装)。
(3)封装可以使对象拥有完整的属性和方法(类中的函数)。
继承:主要实现重用代码,节省开发时间,新创建的类就可直接复用之前类中的所有成员。
多态: 指不同对象接收到同一消息时会产生不同的行为(一个接口,多种方法),简单来说,就是在同一个类或继承体系结构的基类与派生类中,用同名函数来实现各种不同的功能。
5. 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。 在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
6. 类的实例化
用类创建对象的过程,称为类的实例化。
1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
2. 一个类可以实例化出多个对象, 实例化出的对象占用实际的物理空间,存储类成员变量。
int main()
{
Person._age = 100; // 编译失败: error C2059: 语法错误:“.”
return 0;
}
Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。
3. 做个比方: 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象 才能实际存储数据,占用物理空间。
7. 类对象模型
7.1 如何计算类对象的大小
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算 一个类的大小?
7.2 类对象的存储方式猜测
(1)如果包含对象中所有成员:成员变量+需要调用的函数。
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一 个类创建多个对象时, 每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么 如何解决呢?(2)代码只保存一份,在对象中保存存放代码的地址。
(3)只保存成员变量,成员函数存放在公共的代码段。
问题:对于上述三种存储方式,那计算机到底是按照那种方式来存储的?
我们再通过对下面的不同对象分别获取大小来分析看下:
// 类中既有成员变量,又有成员函数
class A1
{
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
sizeof(A1) : __4____ sizeof(A2) : ____1__ sizeof(A3) : ___1___
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
7.3 结构体内存对齐规则
为什么要内存对齐
(1)性能原因:为了处理器对内存访问更加快速便捷,提升性能。
(2)平台原因:某些硬件平台只能在特定地址处访问,否则抛出硬件异常。
1. 第一个成员在与结构体偏移量为0的地址处。
2. 第二个成员变量要对齐到该成员对齐数的整数倍的地址处。
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值,VS中默认的对齐数为8)
8. this指针
8.1 this指针的引出
我们先来定义一个日期类 Date:
class Date
{
public:
void Init(int mouth,int day,int year)
{
_mouth = year;
_day = day;
_year = year;
}
void print()
{
cout << _mouth << " " << _day << " " << _year << endl;
}
private:
int _mouth;
int _day;
int _year;
};
int main()
{
Date d1;
Date d2;
d1.Init(2,1,2024);
d2.Init(2,2,2024);
d1.print();
d2.print();
return 0;
}
输出:
对于上述类,有这样的一个问题:我们都知道类的成员函数可以访问类的数据(限定符只是限定于类外的一些操作,类内的一切对于成员函数来说都是透明的),那么成员函数如何知道哪个对象的数据成员要被操作呢,原因在于每个对象都拥有一个指针:this指针,通过this指针来访问自己的地址。
8.2 this指针的特性
- this指针的类型:类类型*const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用。
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传。
- this指针并不是对象的一部分,this指针所占的内存大小是不会反应在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型,
在看下面d1作为实参传递给this形参,所以取他们的地址是相同的。
class Date
{
public:
void Init(int year,int mouth,int day)
{
_year = year;
_mouth = mouth;
_day = day;
cout << this << endl;
cout << " " << endl;
}
void print()
{
cout << _year << "-" << _mouth << "-" << _day << endl;
cout << this << endl;
cout << " " << endl;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
d1.Init(2024,3,28);
d1.print();
printf("%p\n",&d1);
return 0;
}
8.3 this指针存在哪里?
答:this指针是个形参,所以跟普通参数一样存在函数调用的栈区里。
8.4 判断下列代码理解this指针
(1)下面的程序的运行结果是?
A. 编译报错
B. 运行崩溃
C. 正常运行
class A
{
public:
void Show()
{
cout << "show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();
(*p).Show(); //这两种情况是一样的
return 0;
}
结果:C
原因:Show()函数是存在公共代码区中,编译的时候在公共代码区中找到这个函数,和普通的函数调用是一样的,只需要call函数地址就行。我们发现这里p是空指针,传过去的this指针只是接收了p的空指针,就类似于this指针被初始化为空指针。这是允许的,调用Show函数的时候没有用到this指针的解引用。
(2)下面的程序的运行结果是?
A. 编译报错
B. 运行崩溃
C. 正常运行
class B
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
B* p2 = nullptr;
p2->PrintA();
return 0;
}
结果:B
原因:B* p2 = nullptr; 相当于把this = nullptr,此程序崩溃是在PrintA()中,会隐含一个this->_a,而this指针是一个空指针,访问this指针_a的位置,就要对空指针进行解引用,此时就会崩溃,当调用PrintA() 函数的时候,用到了this->_a。
本章完