文章目录
【C++】类和对象[上]
1.面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
举个例子:比如我们要完成一个外卖系统:
如果我们按面向过程,会分为几个过程:1.上架商品、2.点外卖、3.分配骑手、4.派送、5.点评…
如果我们按面向对象,则是我们会设计几个类型,也就是对象:1.商家 2.用户. 3.骑手,我们就会关注这几个对象之间的关系,关注他们是怎么去交互,然后完成这整个外卖过程。
2.类的引入
在C语言中,我们习惯用struct来定义一个结构体,且在结构体中,我们只能定义变量
struct Student
{
char name[20];
char gender[3];
int age;
};
但在C++中,将struct升级成为了类,并且C++兼容C语言中struct的用法
在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct Student
{
void SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintStudentInfo()
{
cout<<_name<<" "<<_gender<<" "<<_age<<endl;
}
char _name[20];//这里加 _ 是为了标识成员变量,防止初始化命名混乱
char _gender[3];
int _age;
};
int main()
{
Student s;
s.SetStudentInfo("zhangsan", "男", 18);
return 0;
}
比起用结构体定义类,在C++中更喜欢用class来代替
3.类的定义
class className(类名)
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
- class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号**。**
- 类中的元素称为类的成员:类中的数据称为类的属性或者成员变量**; 类中的函数称为类的方法或者成员函数
3.1类的俩种定义方式
- 声明和定义全部放在类体中
注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
//人
class Person
{
public:
//显示基本信息
void shouinfo()
{
cout<<_name<<"-"<<_sex<<"-"<<_age<<endl;
}
public:
char*_name;//姓名
char*_sex; //性别
int _age; //年龄
};
在上面人的类里,我们放了函数,和成员变量。 细心的同学就有疑问了,为什么成员变量定义在函数下面,为什么函数还可以调用,编译器不是从上往下找的吗?
因为我们定义在了同个类里,是属于同一个类域,无论定义前后都是可以调用的。
- 声明放在.h文件中,类的定义放在.cpp文件中
//student.h
//学生
class Student
{
public:
void SetStudentInfo(const char* name, const char* gender, int age);
void PrintStudentInfo();
public:
char _name[20];
char _gender[3];
int _age;
};
//Student.cpp
#include "student.h"
void Student::SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void Student::PrintStudentInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
一般情况下,更期望采用第二种方式
4.类的访问限定符及封装
4.1访问限定符
从上面的代码,有的同学会发现一个关键字,public,这个就是C++中的访问限定符,
除了public(公有),还有private(私有),protect(保护)。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
而这三个访问限定符有什么功能呢?
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- class的默认访问权限为private,struct则为public(因为struct要兼容C)
注意:
访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。
struct和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。
4.2封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装的本质是一种管理。 我们使用类数据和方法都封装到一下。 不想给别人看到的,我们使用 protected/private 把成员 封装 起来。 开放 一些共有的成员函数对成员合理的访问。
所以封装本质是一种管理。
举个例子:我们的旅游景区通常需要管理,如果没有管理,就任由旅客肆意破坏了,所以我们需要一个景区的规划将景区围起来,完成封装,如果完全封装的话,旅游景区就没有意义了,所以我们开发了景区大门和观光通道(访问限定符),可以买票突破封装在合理的监管机制下进去参观。
在C语言中,大多数情况中调用者和定义结构者不是同一个人,就可能会存在调用者测出bug的可能。对于C++而言,C语言比较自由开放,但过于放纵就导致了一些问题。
//C语言中数据和方法是分离的
struct Stack
{
int* _a;
int _top;
int _capacity;
};
void StackInit(struct Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = 0;
ps->_top = 0;
}
void StackPush(struct Stack* ps, int x)
{
}
struct Stack StackTop(struct Stack* ps)
{
}
int main()
{
struct Stack st;
StackInit(&st);
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
printf("%d\n", StackTop(&st));
printf("%d\n", st._a[st._top]); //可能就存在误用
printf("%d\n", st._a[st._top - 1]); //可能就存在误用
}
以上是C语言实现的一个栈,在主函数中,我们想要访问栈顶的元素。在常规情况下,我们调用StackTop函数即可访问到栈顶元素。但是我们也可以使用访问数组下标的方式拿到栈顶元素,此时如果调用者不清楚使用者的定义方式,就有可能存在误用。
例如:这段代码我们定义_top是栈顶元素的下一个元素的下标,因此栈顶元素的下标应该是_top-1,而调用者如果误以为top就是栈顶元素的下标,即有可能存在误用。因此这里太过自由。
为了解决这一问题,在C++中,结构体不仅可以定义变量,还可以定义函数。我们如果把函数定义在类中,我们把成员变量封装在类中,外界函数无法调用。因此如果此时我们想调用栈顶元素,我们只能调用Top函数的接口。这就避免了上述问题的发生。
class Stack
{
private:
void Checkcapacity()
{
}
public:
void Init()
{
}
void Push(int x)
{
}
void Top()
{
}
private:
int* _a;
int _top;
int _capacity;
};
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.类的实例化
用类类型创建对象的过程,称为类的实例化
-
类只是 一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没有分配实际的内存空间 来存储它
-
一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量
做个比方。 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图 ,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
7.类对象模型
7.1如何计算类对象的大小
在我们写的类中,一般有四种情况:1.包含成员函数和成员变量的,2.只包含函数 3.只包含成员变量的 4.空类
在C语言中,我们学结构体的时候,我们可以计算结构体(只定义变量)的大小
然后我们在看在C++中,stack类的大小(定义了成员变量和成员函数)
我们的结果还是一样的。
其实真相就是类对象的存储方式只保存成员变量,成员函数存放在公共的代码段。
为什么这么设计呢?我们的类就是一份建筑图纸,我们建立每一个对象就相当是每一个房子,都是不一样的,但用的都是同一种方法(init(),Top()),所以我们没有必要把每个函数都存储一份。
一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类
8.this指针
8.1引入this指针
上面我们提到,我们用很多的建筑图纸(类) ,建了很多房子(对象),那我们该怎么区分他们呢? C++为了解决这个问题,就设计添加了this指针来解决这个问题。
即:
C++编译器给每个非静态的成员函数*增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
8.2this指针的特点
-
this指针的类型:类名* const
-
只能在“成员函数”的内部使用
-
this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this 形参。所以对象中不存储this指针。
-
this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
8.3显式使用this指针
void PrintStudentInfo() { cout<<_name<<" "<<_gender<<" "<<_age<<endl; }
我们在写这个函数的时候,是这样的,
但实际上,运行调用这个函数 ,编译器会做如下处理:
void PrintStudentInfo() { cout<<this->_name<<" "<<this->_gender<<" "<<this->_age<<endl; }
8.3this指针题目练习
class A
{
public:
void Show()
{
cout << "show()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();
return 0;
}
以上的程序的运行程序结果是? A,编译报错 B运行崩溃 C.正常运行
答案是:C
解析:我们可以看到show()函数是我们的成员函数,是存在公共代码区中,编译的时候在公共代码区中找到这个函数,所以,无论是哪个对象去调函数都是一样的,只需要call函数地址,然后我们可以看到这里的p指针是空指针,传过去的this指针(存在栈区中)(寄存器)只是接收了p的空指针,就类似于this指针被初始化为空指针。这是允许的
class B
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
B* p2 = nullptr;
p2->PrintA();
return 0;
}
上面的程序的运行结果是? A.编译报错 B.运行崩溃 C.正常运行
答案:B
解析:此程序崩溃是在PrintA()中,会隐含一个this->_a,而this指针是一个空指针,访问this指针_a的位置,就要对空指针进行解引用,此时就会崩溃。我们也可通过调试观察到。
注意:我们的this指针是存在栈区中,因为this指针是个形参,形参是在函数的栈桢里,在函数的栈桢里面的变量是属于栈中的
结语
以上是类和对象的第一部分内容,后面还会更新有更加深入的解析。