C语言
是面向过程的,关注的是过程,在解决问题时往往注重求解问题的步骤,通过函数调用逐步求解问题。
C++
是基于面向对象的,解决问题时往往关注的是解决问题的对象
c++是兼容c语言的,因此c++并不是纯面向对象,而是基于面向对象,c++也可以和c语言一样面向过程;Java是纯面向对象的语言。
struct Stack
{
int* a;
int top;
int capacity;
};
int main()
{
struct Stack st;//c语言中struct Stack是一个类型,不能直接使用Stack定义变量
//Stack st;//error
typedef struct Stack Stack;//c语言必须使用typedef将struct Stack重定义为Stack
Stack st;//正确
return 0;
}
- c语言中的结构体只能包含成员变量,不能包含函数
- 未使用
typedef
时无法使用Stack
定义变量,必须用struct Stack
定义变量
struct Stack
{
int* a;
int top;
int capacity;
//C++中的结构体可以包含成员函数
void Init()
{
a = nullptr;
top = capacity = 0;
}
void Push(int val)
{
//检查是否需要扩容
if (capacity == top)
{
int newcapacity = capacity == 0 ? 4 : 2 * capacity;
int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
if (tmp)
a = tmp;
}
a[top] = val;
top++;
}
int Top()
{
return a[top - 1];
}
};
int main()
{
struct Stack st1;//C++中结构体定义保留了C语言的方法
Stack st2;//C++中的结构体定义可以直接省略struct
//和访问成员变量一样的方式访问成员函数
st2.Init();
st2.Push(1);
st2.Push(2);
st2.Push(3);
cout << st2.Top() << endl;
}
运行结果:
- C++中的结构体保留了C语言结构体的写法,支持和C语言一样使用
struct Stack
定义结构体- C++中的结构体可以包含成员函数,结构体变量访问成员函数的方法和访问成员变量的方法一样使用
.
操作符或者->
操作符
类(英语:class)在物件导向程式设计中是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的特性和方法。
特性是成员变量;方法是成员函数
这么一看,那class
是不是和c++中的结构体
一样呢?都具有成员函数和成员变量。
我们将定义Stack时的
struct
替换成class
class Stack { int* a; int top; int capacity; //C++中的结构体可以包含成员函数 void Init() { a = nullptr; top = capacity = 0; } void Push(int val) { //检查是否需要扩容 if (capacity == top) { int newcapacity = capacity == 0 ? 4 : 2 * capacity; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp) a = tmp; } a[top] = val; top++; } int Top() { return a[top - 1]; } }; int main() { struct Stack st1;//C++中结构体定义保留了C语言的方法 Stack st2;//C++中的结构体定义可以直接省略struct //和访问成员变量一样的方式访问成员函数 st2.Init(); st2.Push(1); st2.Push(2); st2.Push(3); cout << st2.Top() << endl; }
❓ 请思考:怎么将
struct
替换为class
后Push不可以访问了?
如果任何一个类的成员(成员函数、成员属性)我们在类的外部都可以随意访问,那么是不是会造成不安全?比如有Stack
类,我们规定只能取出栈顶的元素,如果Stack
中的成员属性可以随机访问,那么我们在类外部是不是可以取a[0],a[1]……呢?那是不是就不符合栈的特点了,所以我们引入了访问限定符。
public
修饰的成员可以在类外部随意访问protected
修饰的成员不可以在类外部随意访问,可以在类内部访问private
修饰的成员不可以在类外部随意访问,可以在类内部访问如何使用访问限定符呢?
拿class Stack
举例子,我们只想让别人访问Stack
的顶部元素,不能访问其他任意位置,因此我们将Top函数设置为public
访问属性;而成员属性a,top,capacity全部设置为private
访问属性,使它们在类外部无法直接访问,当我们要取出栈里元素时只能调用public
的Top函数,这个函数就是取出栈顶元素,换句话说,我们通过访问限定符限制了我们只能取出栈的顶部元素。
再回到上面的代码,为什么将struct
替换成class
后显示Push
不可以访问呢?那是不是说明了此时Push
成员函数的访问属性为private
或者protected
,没错**,**
结论1:对于class
来说若没有显示规定访问属性则默认为private
,对于struct
来说若没有显示规定访问属性则默认为public
(因为c++需要兼容c语言的struct)
尝试将
class
的成员函数设置为public访问属性class Stack { int* a; int top; int capacity; public:// 成员方法设置为public void Init() { a = nullptr; top = capacity = 0; } void Push(int val) { //检查是否需要扩容 if (capacity == top) { int newcapacity = capacity == 0 ? 4 : 2 * capacity; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp) a = tmp; } a[top] = val; top++; } int Top() { return a[top - 1]; } }; int main() { struct Stack st1;//C++中结构体定义保留了C语言的方法 Stack st2;//C++中的结构体定义可以直接省略struct //和访问成员变量一样的方式访问成员函数 st2.Init(); st2.Push(1); st2.Push(2); st2.Push(3); cout << st2.Top() << endl; }
运行结果[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5PuDZml-1689904140752)(images/image-20230720102601767.png)]
加上public
后,我们仍然不能在类外部访问成员属性[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNkny7WL-1689904140752)(images/image-20230720102835303.png)]
结论2:成员访问限定符的作用范围是从当前位置开始持续到下一个访问限定符,若没有下一个访问限定符则对接下来所有的成员都作用
class Stack
成员属性a,top,capacity都是默认的private
访问控制权限,持续到成员函数Init
,因此不能在类外部直接访问成员属性。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限
来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类定义了一个新的作用域,类的成员属性就属于该作用域中声明的变量,若要在类外部访问该类的属性,需要使用作用域操作符::
来指明该变量属于哪个类
例如在Stack.h文件中声明
class Stack
//Stack.h文件中 class Stack { private: int* a; int top; int capacity; public: void Init(); void Push(); int Top(); };
在Stack.cpp文件中定义
class Stack
//Stack.cpp文件中 //使用了Stack中的成员属性需要指名类域为Stack void Stack::Init() { a = nullptr; capacity = top = 0; } //使用了Stack中的成员属性需要指名类域为Stack void Stack::Push(int val) { //检查是否需要扩容 if (capacity == top) { int newcapacity = capacity == 0 ? 4 : 2 * capacity; int* tmp = (int*)realloc(a, sizeof(int) * newcapacity); if (tmp) a = tmp; } a[top] = val; top++; } //使用了Stack中的成员属性需要指名类域为Stack int Stack::Top() { return a[top - 1]; }
使用类类型定义对象的过程称为类的实例化,例如class Stack st;
,我们使用类型Stack
定义了一个对象st
,这个过程称为Stack
的实例化。
Stack st1;Stack st2;
每一个对象都具有独立的存储空间类和结构体很相似,结构体的大小是所有成员变量加起来的大小并进行内存对齐,那么类的大小是不是和结构体一样呢?
我们来看不同的类的大小
//既有成员函数又有成员属性 class A { private: char b; public: void f() {}; }; class B { private: char b; int a; public: void f() {}; }; //只有成员属性 class C { private: char b; int a; }; //只有成员函数 class D { public: void f() {}; }; //空类 class E { }; int main() { cout << sizeof(A) << endl; cout << sizeof(B) << endl; cout << sizeof(C) << endl; cout << sizeof(D) << endl; cout << sizeof(E) << endl; }
结论:
第一个成员在与结构体偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
结构体内存对齐
类的大小只算类中的成员属性,那么使用类定义对象时,对象中的成员方法存放在哪里❓
由于同一个类的成员函数只有一份,因此将一个类中的成员函数与全局函数共同存放在公共代码区。当对象调用该函数时会去公共代码区寻找该函数的执行指令
不同对象调用同名的成员函数是同一个,它存放在公共代码区
结论:存储对象的空间中只会存储该对象所属类的成员属性,不会存储成员函数,若要调用成员函数就要去公共代码区寻找该函数。
我们来看
Date
类的定义class Date { private: int _year; int _month; int _day; public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << " " << _month << " " << _day << endl; } }; int main() { Date d1, d2; d1.Init(2023, 7, 20); d2.Init(2022, 8, 10); d1.Print(); d2.Print(); }
上述代码存在一个问题:前面提到过,类中的函数存放在公共代码区
,也就是d1.Init
和d2.Init
调用的是同一个函数,但是Init
函数中的代码为_year = year;_month = month;_day = day
,再调用Init函数时,编译器是如何知道此时的_year,_month,_day
是当前对象的成员属性呢?也就是说d1.Init(2023, 7, 20)
是如何将2023赋值给d1的_year
的呢?
在C++中,每一个类的成员函数在调用时编译器都会默认隐式传一个参数,该参数为this指针
,用来指向调用函数的当前对象。
例如,Init成员函数
的定义在编译器看来是这样的
void Init(Date* this, int year, int month, int day)//编译器隐式传递this指针
{
this->_year = year;
this->_month = month;
this->_day = day;
}
而调用语句d1.Init(2023,7,20)
会被编译器转换为d1.Init(&d1, 2023, 7, 20)
,因此编译器会根据this指针访问该对象的成员属性
前3步是将参数20,7,2023依次压栈,第4步是将d1的地址存放在寄存器ecx中(&d1并没有压栈而是存放在寄存器中)
所以,this指针是用来简化我们调用成员函数的步骤,使我们不需要显示的传递对象地址,而传递对象地址这件事由C++编译器帮我们完成。
总结:
C++编译器给每一个非静态成员函数添加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中访问所有“成员属性”的操作都由该指针变量完成,所有操作对用户来说都是透明的,即用户不需要显示的传递该指针参数,由编译器自动完成
*const this
,即在成员函数中不可以更改this指针的指向。寄存器ecx
自动传递。register
中,VS中C++编译器将this指针存放在寄存器ecx
中练习
1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
解释:
前面说过,成员函数是存放在公共代码区的,调用成员函数时只会区公共代码区寻找函数代码段,即使这里this指针
为空,也不会对空指针进行解引用,而我们可以观察上述代码的汇编指令
2.下面程序编译运行结果是? A、编译报错 B、运行崩溃C、正常运行
class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
解释:
函数调用不会出错,问题在于this指针
所指向对象的_a
成员,这就造成了解引用空指针,程序会崩溃。