一、面向过程与面向对象
C语言作为一种面向过程的编程语言,注重解决问题的过程和步骤,通过函数和控制流程的设计来组织程序。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
现实实例:制作一个简单的三明治。
面向过程分析:(C语言)
1.收集所需材料和工具:面包、黄油、火腿、生菜、刀子、砧板等。
2.将切好的面包放在砧板上。
3.使用刀子涂抹黄油在面包片上。
4.在其中一片面包上放上火腿和生菜。
5.将另一片面包盖在火腿和生菜上,使之成为一个完整的三明治。
6.可选:将整个三明治切成两半或四等份。
7.完成。
面向对象分析:(C++)
1.定义一个"三明治"类,它具有属性(面包、黄油、火腿、生菜)和方法(涂抹黄油、放置火腿和生菜、组装成三明治)。
2.创建一个"三明治"对象。
3.调用对象的方法,按照特定的顺序执行:
● 调用涂抹黄油的方法,在面包片上涂抹黄油。
● 调用放置火腿和生菜的方法,在其中一片面包上放置火腿和生菜。
● 调用组装成三明治的方法,将另一片面包盖在火腿和生菜上。
4.可选:调用切割的方法,将整个三明治切成两半或四等份。
5.完成。
在面向过程分析中,我们按照步骤逐一执行操作,强调流程和步骤的线性顺序。
而在面向对象分析中,我们将问题抽象为一个对象,该对象具有属性和方法,通过调用对象的方法来实现功能,强调对象的行为和内部状态的封装。
总之面向对象以后,重点不再关注做事的具体过程,而是关注其中涉及哪些对象
二、类
2.1 类的介绍
还记得C语言阶段学习过的结构体吧?在结构体中我们可以定义各种类型的变量,但是我们不能在结构体中定义函数.
C语言中:
同样一段代码在C++中,结构体内不仅可以定义变量,也可以定义函数。
C++中:
为什么呢?因为C++中将结构体升级为了==“类”.在类==中是可以定义函数的,通常被称为成员函数.
在C++中,class关键字用于定义一个类。类是一个用户定义的数据类型。
类体中内容称为类的成员:可以包含属性(成员变量)和操作/方法(成员函数)。
2.2 类的定义方式
使用class关键字可以创建一个新的类,并定义它的特征(如数据成员和成员函数)。类可以用于封装数据和行为,并提供对外部程序的接口。通过类的实例化,可以创建对象,并访问其成员变量和成员函数。在面向对象编程中,类是非常重要的一个概念,它使得程序更加模块化,易于维护和扩展。
(1)声明和定义全部放在类体中.
注意:成员函数如果在类中定义,编译器默认是按内联函数(inline)处理.(同样如果函数体过长也是不会产生内联的.)
#include <iostream> using namespace std;//在工程代码中不建议展开可能会产生命名冲突 class Person { public: // 构造函数(后面会讲,这里按普通成员函数理解) Person(char* n, int a) { name = n; age = a; } // 成员函数 void introduce() { cout << "欢迎来到CSDN!\n我是" << name << ",我的年龄是" << age << "岁" << endl; } private: //成员变量 char* name; int age; }; int main() { char name[] = "初阶牛"; Person person(name, 18); person.introduce(); return 0; }
运行结果:
欢迎来到CSDN! 我是初阶牛,我的年龄是18岁
这个类的名字叫做 Person,它有两个私有成员变量:name 和 age。
类还有一个公有的成员函数:introduce。introduce 函数用于打印出个人信息,即打印出对象的 name 和 age 属性。
在 main 函数中,我们创建了一个名为 person 的 Person 对象,并通过构造函数初始化了它的成员变量。然后我们调用了 introduce 函数来展示个人信息。
通过使用成员函数和成员变量,我们可以对对象进行操作和访问其属性,从而使类具有更多的功能和灵活性。请注意,在 C++ 中需要使用 iostream 库进行输入输出操作,并使用 main 函数创建类的对象并调用成员函数。
(2)类的声明和"成员函数"分离
即类声明放在.h文件中,成员函数定义放在.cpp文件中.
注意:成员函数名前需要加类名::
2.3 类的访问限定符
在C++中,类的访问限定符(访问修饰符)用于控制类的成员对外部代码的可见性和访问权限。C++提供了三个主要的访问限定符:public、private和protected。
10公共访问(public):使用public关键字来指定。公共成员可以从任何地方访问,包括外部代码和其他类。公共成员在整个程序中可见。
2.私有访问(private):使用private关键字来指定。私有成员只能在声明它们的类内部访问。其他任何外部代码或其他类都无法直接访问私有成员,包括子类。
3.受保护访问(protected):使用protected关键字来指定。受保护成员只能在声明它们的类内部访问以及该类的子类中访问。外部代码无法直接访问受保护成员。
我们暂时这里将私有访问(private )和受保护访问(protected)看作相同的,后续再区分.
注意:
1.C++中class(类)的默认访问级别是私有访问(private)。类的成员将默认为私有成员,只能在类内部访问。
2.struct(结构体)为public(因为struct要兼容C语言),在C语言中,外部可以访问结构体中的成员变量.
访问限定符的选择取决于设计需求和封装原则。公共成员允许类的用户直接访问,而私有成员则隐藏了实现细节并提供了更好的封装。受保护成员专门用于派生类访问,并且在类外部不可见。
2.4 封装的介绍
封装的定义:(灰常重要)
是指将数据和方法放在一起.将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
在现实世界中,手机是一个复杂的设备,它包含了许多内部组件和功能,如屏幕、摄像头、声音、通信等。对于我们普通用户来讲,手机只需要提供给我们我们点击的屏幕,和手动控制开关机的按键就可以了,它内部具体是怎么实现功能的我们并不关心,如果让用户去关心CPU如何设计,主板上的线路如何布局,这显然是不合理的,手机的封装也就体现了管理,帮助用户更方便的使用手机
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
三、“类” 与 “对象” 之间的关系
我们先看一下test2函数.
class Person { public: //成员函数 Person(char* n, int a); void introduce(); private: //私有成员变量 char* name; int age; public: float weight; }; void test2() { //报错 Person.weight = 60.5;//报错,类只是声明,并没有申请空间,不能用于存放数据 //正确写法 char name[] = "初阶牛"; Person cjn(name, 18);//通过类实例化出 cjn这个对象. cjn.introduce(); cjn.weight = 60.5;//实例化出来的对象是有空间的,可以存储数据 }
1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
2.一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
有一个很形象的比喻,类就好比是建筑的图纸,类只是一些声明,并不没有去申请实际的空间,就好比图纸只是设计形状,并没有占有空间.类不能存储数据,就类似于图纸不能住人.
通过类实例化出的对象后就分配的实际空间,对象可以用于存储数据,就像图纸设计出来房子后,房子里面就可以住人了.
3.1 类的大小计算
试着猜一下下面People类的大小.
#include <iostream> using std::cout; using std::cin; using std::endl; class People { public: void Print() { int a = 0; double b= 1.2; cout << _name << endl; } private: char _name; int _age; }; int main() { cout << sizeof(People) << endl; return 0; }
运行结果:
8
解释:
为什么是8呢?因为类在计算大小时也要考虑内存对齐.
char _name占1个字节(偏移量为0),int _age占四个字节(4-7偏移量).共八个字节.
为什么不计算成员函数的大小呢?
那就要说到类的设计方式了,因为成员函数消耗的内存相对都比较大,而每个对象都是使用同一个成员函数,如果每个对象都给成员函数开辟空间,这就比较浪费了,所以C++中的类采用下图这种方式存储:
将;类的成员函数放在公共代码段,需要使用的时候调用即可,对象之间公用同一个成员函数.这种设计方式有效的节省了类实例化出对象后的空间消耗.
那小伙伴掌握如何计算类的大小了吗?
不妨猜一下下面A类和B类的大小.
// 只有成员函数的类 class A { public: void test() {} }; //空类 class B {}; int main() { cout << sizeof(A) << endl; cout << sizeof(B) << endl; return 0; }
运行结果:
1 1
这是因为没有成员变量的类或者空类也是会在占用一个字节,因为需要占位,表示对象的存在.
3.2 this指针
#include <iostream> using std::cout; using std::cin; using std::endl; class Person { public: //成员函数 Person(char* n, int a) { _name = n; _age = a; } void introduce() { cout << _name << endl; } private: //私有成员变量 char* _name; int _age; }; int main() { char name1[] = "初阶牛"; char name2[] = "CSDN"; Person person1(name1, 18); Person person2(name2, 18); //这两个调用的是同一个函数吗? person1.introduce(); person2.introduce(); return 0; }
运行结果:
初阶牛 CSDN
上面这段代码中这两个调用的是同一个函数吗?如果是同一个,为什么打印的结果却不一样?
person1.introduce(); person2.introduce();
解释:
调用的是同一个函数,之所以打印的结果不一样是因为C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针(this指针)参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
没理解的看过来:
其实就是编译器帮我们传了参,不需要用户进行手动传参.
结构体内存对齐复习:传送门
3.3 深入理解this指针
为了深入理解this指针,下面有两道题可以做一下:
第一题:this指针本身存储在哪里?
A. 栈区
B. 堆区
C. 对象中
D. 常量区
答案:
栈区,因为this指针就是一个形参,只不过是被编译器默认传递,形参是存放在栈区的 函数栈帧建立时压栈,函数结束时,销毁.
第二题:下面这两段代码分别会出现什么情况?
//代码1: class A { public: void Print() { cout << "HELLO CSDN" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
// 代码2: class A { public: void Print() { cout << _age << endl; } private: int _age; }; int main() { A* p = nullptr; p->Print(); return 0; }
答案:
代码1:
正常运行,虽然this是空指针,但是并没有对this指针进行解引用,传递空指针是不会报错的.
代码2:
运行崩溃,对this空指针进行解引用,属于非法访问.
运行图如下:
四、C与C++对比
对比C语言,帮助更好的理解C++的封装特性.
C语言数据和方法是分离的,给予C程序员很大的操作空间.这样也就使得对C程序员的要求很高.太自由了!
比如:
对于一个用C语言实现的栈.很多数据在栈的外部可以被随意的修改和使用,这样就对程序员的要求极高.对于不规范的编程,(一会通过接口(函数),一会自己在外界直接访问)很容易造成混乱
C++程序员受封装的保护,对于栈中的很多操作只能通过调用对应的接口实现,更好的约束了程序员的操作规范
C实现栈:
//C语言版本 #include <stdio.h> #include <assert.h> #include <stdlib.h> #include <stdbool.h> typedef int DataType; typedef struct Stack { DataType* array; int capacity; int size; }Stack; void StackInit(Stack* ps) { assert(ps); ps->array = (DataType*)malloc(sizeof(DataType) * 3); if (NULL == ps->array) { assert(0); return; } ps->capacity = 3; ps->size = 0; } void StackDestroy(Stack* ps) { assert(ps); if (ps->array) { free(ps->array); ps->array = NULL; ps->capacity = 0; ps->size = 0; } } void CheckCapacity(Stack* ps) { if (ps->size == ps->capacity) { int newcapacity = ps->capacity * 2; DataType* temp = (DataType*)realloc(ps->array, newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc申请空间失败!!!"); return; } ps->array = temp; ps->capacity = newcapacity; } } void StackPush(Stack* ps, DataType data) { assert(ps); CheckCapacity(ps); ps->array[ps->size] = data; ps->size++; } int StackEmpty(Stack* ps) { assert(ps); return 0 == ps->size; } void StackPop(Stack* ps) { if (StackEmpty(ps)) return; ps->size--; } DataType StackTop(Stack* ps) { assert(!StackEmpty(ps)); return ps->array[ps->size - 1]; } int StackSize(Stack* ps) { assert(ps); return ps->size; } int main() { Stack s; StackInit(&s); StackPush(&s, 1); StackPush(&s, 2); StackPush(&s, 3); StackPush(&s, 4); printf("%d\n", StackTop(&s)); printf("%d\n", StackSize(&s)); StackPop(&s); StackPop(&s); printf("%d\n", StackTop(&s)); printf("%d\n", StackSize(&s)); StackDestroy(&s); return 0; }
C++实现栈:
typedef int DataType; class Stack { public: void Init() { _array = (DataType*)malloc(sizeof(DataType) * 3); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = 3; _size = 0; } void Push(DataType data) { CheckCapacity(); _array[_size] = data; _size++; } void Pop() { if (Empty()) return; _size--; } DataType Top() { return _array[_size - 1]; } int Empty() { return 0 == _size; } int Size() { return _size; } void Destroy() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: void CheckCapacity() { if (_size == _capacity) { int newcapacity = _capacity * 2; DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType)); if (temp == NULL) { perror("realloc申请空间失败!!!"); return; } _array = temp; _capacity = newcapacity; } } private: DataType* _array; int _capacity; int _size; }; int main() { Stack s; s.Init(); s.Push(1); s.Push(2); s.Push(3); s.Push(4); printf("%d\n", s.Top()); printf("%d\n", s.Size()); s.Pop(); s.Pop(); printf("%d\n", s.Top()); printf("%d\n", s.Size()); s.Destroy(); return 0; }
最后补充一个小知识:
局部域和全局域会影响生命周期
类域和命名空间域不会影响生命周期.