一.初步认识面向过程和面向对象
面向过程,关注的是怎么去做,比如在外卖系统中,强调点餐,做餐,送餐等一系列动作的方法,反映到语言中是函数方法的实现;而面向对象,更关注的是谁去做,比如在外卖系统中,强调的是商家,买家和送货员之间的交互,反映到语言中则是对象的实现。
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
这里C++由于兼容C语言,因此既是面向过程,又是面向对象的,但是C++更关注的是对象,所以说C++是基于面向对象的。
二.类的引入
在C语言中,结构体只能定义变量,而在C++中,结构体升级为类,既可以定义变量,也可以定义函数:
struct Book
{
void SetInfo(const char* name, const char* writer, double price)//建立书本信息
{
strcpy(_name, name);
strcpy(_writer, writer);
_price = price;
}
void PrintInfo()//打印书本信息
{
cout << _name << endl;
cout << _writer << endl;
cout << _price << endl;
}
char _name[20];
char _writer[20];
double _price;
};
int main()
{
Book b1;
Book b2;
b1.SetInfo("老人与海", "海明威", 12.54);
b2.SetInfo("骆驼祥子", "老舍", 14.88);
b1.PrintInfo();
cout << endl;
b2.PrintInfo();
return 0;
}
上面的结构体struct即为一个类,{}则形成了一个类域,{}中的内容为结构体Book的成员,既有成员变量,又有成员函数。在C++中,类通常用class这个关键字来表示。那么struct和class二者之间有什么区别呢?接下来我们就来介绍类。
三.类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1.定义和声明全部放在类体中,需要注意的是:
成员函数被定义在类体中,编译器会默认将其当作内联函数,其效用等同于函数前加上inline关键字,若不知道何为内联函数,可以参考之前文章中关于内联函数的介绍。
C++入门
2.声明与定义分离
声明放在头文件中,而定义放在源文件中。
通常情况下,为了代码的规范性,更倾向于采用第二种方法来实现类,并且代码较短的成员函数直接定义在类体中,而代码较长的函数定义在类体外。
需要注意的是,类中的成员变量均为声明,它们在实例化之前都未被分配空间,不能称作为定义。
四.类的访问限定符及封装
1.访问限定符
在之前我们讨论到class和struct之间有什么区别,那么这里我们将会介绍,首先,我们来了解以下类的访问限定符及封装。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符的说明:
1.public修饰的成员在类外可以直接被访问
2.protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4.在没有访问限定符的情况下,class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
由上面的说明我们就可以知道struct和class这两个关键字之间的区别在哪里了:实际使用过程中,struct和class定义类时并无区别,只是在二者均没有访问限定符的时候,struct默认的访问权限为public,而class默认的访问权限为private。
2.封装
我们知道面向对象有三大特性:封装,继承,多态。在类和对象阶段,我们研究类的封装特性。
首先,封装指的是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
其次,从本质上来说,封装是一种管理:举个例子,景区如果不加管理的话,那么景区的东西很可能会被不守规章制度的人破坏,这就好比C语言中为被封装的代码随时可能被修改,有时导致出现很大的错误;那么为了加强管理,保护景区,就需要设立景点售票口,同时安装监控和保安来保证景区不被破坏。
类也是如此,对于我们不想被随意修改的成员变量,我们用private表示其为私有,而为了使用者能够合理调用,我们将使用方法封装成一个个的接口即成员函数用public表示其为公用,至此我们将成员封装起来,同时开放一些公有的成员函数对成员合理的访问。所以封装本质是一种管理,使用封装可以是代码更加安全。
五.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
比如,在上面介绍类的第二种实现方式中的代码,在Book.cpp中定义函数ShowInfo时就是指定其为类域Book中的成员函数。
//Book.h
class Book//书
{
public:
void ShowInfo();//展示书的信息
private:
char* _name;//书名
char* _writer;//作者
double _price;//价格
//Book.cpp
#include "test.h"
void Book::ShowInfo()
{
cout << _name << " " << _writer << " " << _price << endl;
}
};
六.类的实例化
用类类型创建对象的过程,称为类的实例化
1.类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
3.打个比方,类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
七.类对象模型
1.计算类对象的大小
类包含了成员变量和成员函数,那么类的大小应该如何计算呢?首先我们来看看下面这个代码的结果是什么:
class Book
{
public:
void ShowInfo(){}
private:
char* _name;
char* _writer;
double _price;
};
int main()
{
cout << sizeof(Book) << endl;
return 0;
}
可以看到,类Book的大小为16,那么这个16是怎么求出来的呢?
2.类对象的存储方式
我们再来看一个代码:
class C1//类中既有成员变量,又有成员函数
{
public:
void fun();
private:
int _a;
};
class C2//类中只有成员函数
{
public:
void fun();
};
class C3//类中什么都没有,即空类
{
};
int main()
{
cout << "C1:" << sizeof(C1) << endl;
cout << "C2:" << sizeof(C2) << endl;
cout << "C3:" << sizeof(C3) << endl;
return 0;
}
它的结果是:
可以看到C1的大小为成员变量_a的大小,C2和C3的大小均为1,说明类的大小并不包括成员函数的大小,实际上如果类实例化时也会给成员函数开辟一块空间,那么当一个类创建多个对象时,每个对象中都会保存一份成员函数的代码,相同代码保存多次,浪费空间。
既然成员函数不在类的大小计算范围内,那么为什么空类的大小为1呢?这是因为一个类创建的时候需要开辟一块空间来占位,因此内存需要开辟一个字节,这个字节的空间是没有意义的,其不存储任何有效数据,但是其标识了空类的存在。
结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
八.this指针
1.this指针的引出
不知道你是否注意到,在C和C++实现栈的代码中,二者的函数参数有些许不同
可以看到,C++的函数参数比起C语言实现的函数都少了一个参数,那么问题来了,在下面代码中s1和s2都调用Init函数时,编译器是怎么区别是哪个变量调用的呢?这就是我们即将要介绍的this指针所起到的作用了。
int main()
{
cpp::Stack s1;
cpp::Stack s2;
s1.Init();
s2.Init();
s1.Push(1);
return 0;
}
实际上,C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
根据调试窗口可以看到this指针就是s1的地址,通过this指针可以访问s1。
2.this指针的特性
1.this指针的类型:类类型* const,比如上面代码中的this指针类型为Stack*
2.只能在“成员函数”的内部使用,this作为一个关键字不能拿它去当作变量的名字,其使用时可以显式的使用,比如:
void Init()
{
this->_a = (int*)malloc(sizeof(int) * 4);
this->_top = 0;
this->_capacity = 4;
}
3.this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。又因为this指针为形参,而形参和函数中的局部变量是存储在函数栈帧中的,因此this指针可以认为是存储在栈中的。
4.this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递.
我们最后再来看一个问题:this指针可以为空吗?
我们还是通过一个代码来看:
class A
{
public:
void Show()
{
cout << "Show()" << endl;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();
p->Print();
return 0;
}
那么p->Show(); p->Print();这两句代码能否运行成功呢?
可以看到第一句代码运行成功了,而第二句代码运行崩溃了。
这是因为成员函数的地址并不存在于对象中,而是存在于公共代码段;而上面的代码中调用函数时将p传给了隐含的this指针,并不会去访问p所指向的空间,就不存在空指针的解引用,因此程序可以并编译成功。而调用Show函数也没有对this指针解引用,因此程序运行成功了;调用Print函数则会对this指针解引用,故程序崩溃了。
结论:对于调用不会对this指针解引用的函数,this指针可以为空;而对于调用会对this指针解引用的函数,this指针不能为空。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!