目录
面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
例如洗衣服的时候:
上述就是一个过程,注重的是过程。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类的引入
什么是类:
对象:实体 或者:眼睛看到的一切都是对象。
类:将抽象出来的结果用c++翻译过来 就是用计算机可以识别的语言去描述你所知道的对象。C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
实现洗衣机的功能:
c语言实现
struct WashMashine { double length; double width; double height; int colour; }; void InitWashMashine(WashMashine*pw) { } void WashClothes(WashMashine*pw) { } void DryClothes(WashMashine*pw) { } void SetTime(WashMashine*pw) { } void Alarm(WashMashine*pw) { }
c++实现:
类:类就是对这些对象进行描述的。
class WashMashine { //对洗衣机属性的描述 double length; double width; double height; int colour; //对洗衣机功能的描述 void InitWashMashine(WashMashine*pw) { } void WashClothes(WashMashine*pw) { } void DryClothes(WashMashine*pw) { } void SetTime(WashMashine*pw) { } void Alarm(WashMashine*pw) { } }; int main() { //w就是对象 WashMashine w; return 0; }
类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
类的访问限定符及封装
访问限定符
【访问限定符说明】
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。
封装
面向对象程序设计有四大特性:抽象,封装,继承,多态
抽象:多一个复杂事务的认识过程
什么是封装?
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
类的实例化
定义:用类类型定义对象的过程
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
类对象模型
如何计算类对象的大小
class Data
{
//一般情况下,成员方法的权限是公有,成员变量是私有
// 成员的权限到底应该是公有还是私有,需要根据应用场景来确定
public:
void InitDate(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
void PrintDate()
{
cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d;
d.InitData(2002,22,4);
d.PrintData();
//d和dd虽然是不同的对象 但是调用的是同一份成员函数
//打印结果不一样
Data dd;
dd.InitData(2002, 22, 3);
dd.PrintData();
cout << sizeof(Data) << endl;//对象中只包含成员对象,计算类大小的时候实际和计算结构体大小的方式一样
cout << sizeof(d) << endl;
cout << sizeof(dd) << endl;
return 0;
}
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐(和计算结构体大小一样)
空类:
class A
{
void func(){}
int _a;
};
class B
{
public:
void func(int)
{}
};
//空类
//空类的大小是多少?
//空类的小大为什么是1不是0?
class C
{
};
int main()
{
cout << sizeof(A) << endl; //4
cout << sizeof(B) << endl; //1
cout << sizeof(C) << endl; //1
B b1, b2, b3;
int a, b, c;
return 0;
}
结论:在主流的编译器中,编译器给空类分配一个字节,用来区分空类创建的不同的对象
vs g++
this指针
this指针的引出
定义一个日期类Date
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 a;
};
int main()
{
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
对于上图:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
这时候就是c++通过引入this指针来解决这个问题,即C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
看下图具体解读:
当我们用d1来调用时,这时候就会把d1对象的地址交给this指针,最后所有的成员的变量都是通过this来进行访问的(里面的年月日这些都是通过this来调用)。当要d2对象时,就会把d2对象的地址给this指针。
this指针的特性
1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动递,不需要用户传递 。
在vs2013环境中:
注意:在vs中并不是所有成员函数的this都是通过ecx寄存器来传递的。
对于上面的和函数的调用和上述InitDate比较
this指针存储在哪?
this指针存储在栈上
下面就是验证的代码:
this指针能否为空
class A
{
public:
void Print()
{
cout << this << endl;
cout<<_a<<endl;
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A a;
a.Print();
A*pa = &a;
pa->Print();
pa = nullptr;//相当于给this指针传了一个空
pa->Print();
return 0;
}
根据上述代码进行验证得出:this指针可以为空。
但是,this指针为空代码会崩溃吗?结果为不一定。如果this指针为空时调用成员方法时没有访问成员变量就不会崩溃,如果访问成员变量了就会崩溃。因为所有成员变量都是通过this指针进行访问的。
常见的小问题
int a = 10;
int main()
{
printf("%d", a);
return 0;
}
对于上述代码,int a=10; 如果在下面编译就会报错,识别不了。为什么会出现这个问题?
变量在使用时,编译器必须先要看到该变量的定义或者声明。但是:在类的定义中,成员变量的定义在后,成员变量在函数中的使用在前,为什么编译器不报错要搞清楚这个问题:必须知道编译器是如何处理类的。
编译器是如何处理类:
1.识别类名。
2.编译器再识别类中的成员变量。
3.最后识别类中的成员方法 & 并且会对类中的成员方法进行修改。修改:a.还原this指针的参数 b.所有的成员变量通过this来访问
C语言和C++实现Stack对比
C语言实现
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语言中:
(1)每个函数的一个参数都是Stack*
(2)函数中必须对第一个参数检测,因为该参数可能会为NULL
(3)函数中都是通过Stack* 参数操作栈
(4)调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分 离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
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; }
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被 调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传 递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack * 参数是编译器维护的,C语言中需 用用户自己维护。