C++是一门面向对象的编程语言,那类与对象也就是C++的核心部分,现在让我们一起来学习类与对象吧。
定义一个类我们可以用到关键字struct
和class
,在C语言中我们都学过结构体,对struct
并不陌生。但是在C++中的struct
与C中的结构体又有一些差距,可以这么说,就是因为C++兼容了C,才将struct
保留下来。在C语言中的结构体,只能定义变量,使用变量,而在C++中的结构体,既可以定义变量,也可以定义函数。这就是C++中的类。
定义格式:
//struct 为定义类的关键字,ClassName为类的名字,{}中为类的主体
struct className
{
// 类体:由成员函数和成员变量组成
};
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
struct A
{
//成员函数
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;
};
但是在工作中,要完成一个大项目,往往都是声明和定义分离,那如何把类的声明和定义分离呢?声明部分引入成员变量和成员函数的声明,在定义部分通过类名 :: 函数名
来定义
test.h文件中
struct A
{
void SetStudentInfo(const char* name, const char* gender, int age);
void PrintStudentInfo();
char _name[20];
char _gender[3];
int _age;
};
test.cpp文件中
#include "test.h"
//告诉编译器该函数属于类A中的成员函数
void A::SetStudentInfo(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void A::PrintStudentInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
类的限定符有3种
1、public
公有
2、protected
保护
3、private
私有
初学阶段暂时把保护和私有看成是等价的。
类可以看做是一间房间,里面住着一家人,只有这家里的人才有劝访问家。这里的public
相当于把家的大门打开了,谁都可以访问,而private
是把门锁住了,谁都不能访问。但是在房间里了的人,可以互相访问。例如有public
修饰的成员函数可以被外部访问,被private
修饰的成员变量无法被类外访问。但是在同一个类中的成员函数可以访问成员变量。
struct A
{
public:
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;
}
private:
char _name[20];
char _gender[3];
int _age;
};
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
在C++中,我们更喜欢用关键字class
来定义变量,class定义类和struct定义类的区别就是默认的限定符,class默认限定符是private,struct默认的限定符是public(兼容C)。
面向对象的三大特性:封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。
如何理解封装呢?
在现实生活中,相信大家都去过博物馆吧,当我们买票进入博物馆,我们参观博物馆,但是我们不能去修改或者去改变参观物的模型或者去乱写某某到此一游。再举一个例子和数据结构有关的。例如一个栈,栈中有入栈出栈等函数,也有栈的大小和容量。如果我们不进行封装,类外的人就可以随意修改栈的大小和容量,这样会导致我们无法正常操作这个类,如果进行了封装,那我们只能操作的是栈的一些函数,例如出栈入栈。
类就相当于盖楼前的图纸,并不能住人,只有当建造师通过图纸来实现真实的楼,才可以住人。而这里的类也一样,是有类被实例化了,才可以使用。每栋楼都有这些属性和函数,但是实例化后每栋楼的属性的可能不一样。一个类可以实例化出多个对象。
我们拿上一个代码来演示:
struct Student
{
public:
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;
}
private:
char _name[20];
char _gender[3];
int _age;
};
int main()
{
//实例化出一个对象,名为black
Student black;
//通过.来访问对象的成员函数,如果对象为指针,可以通过->来访问成员函数
black.SetStudentInfo("白衬衫i", "男", 22);
black.PrintStudentInfo();
return 0;
}
类的计算大小与C语言中的结构体大小计算方法是一样的。也是通过字节的对齐来计算。如果有不了解的读者,可以通过下面的链接来学习。
如何计算结构体大小.
但是在类中有成员函数,那成员函数会不会影响类的大小呢?
在C++中,类的成员方法是存在公共代码段中的,因为每个对象中成员变量是不同的,但是调用同一份函数,如果不存在公共代码段,当一个类实例化多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。所以类的成员函数实例化后是不会影响类的大小的,也及时不占空间的。
我们通过4个类来对类的大小进行测试:
class A
{
public:
void print()
{
cout << "A::print()" << endl;
}
private:
int _a;
};
class B
{
private:
int _b;
};
class C
{
public:
void print()
{
cout << "C::print()" << endl;
}
};
class D
{};
int main()
{
cout << "有函数有属性:" << sizeof(A) << endl;
cout << "有属性没函数:" << sizeof(B) << endl;
cout << "有函数没属性:" << sizeof(C) << endl;
cout << "空类:" << sizeof(D) << endl;
}
测试结果:
通过前两个类我们可以得出类的成员函数时不占空间的,那为什么当只有函数时和空类却有大小,大小为什么为1呢?编译器给了空类一个字节来唯一标识这个类,表示这个类的存在。
扩展:
1、结构体进行内存对齐的原因:
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。
2、如何让结构体按照指定的对齐参数进行对齐:
在结构体定义前加上#pragma pack(n)
n为指定对齐数
3、如何知道结构体中某个成员相对于结构体起始位置的偏移量
通过地址相减 (char*)&A.member - (char*)&A
A为类名,member为类的某个成员
4、什么是大小端?如何测试某台机器是大端还是小端
大端存储模式:就是内存的低地址上存着数据的高位,高地址上存着数据的低位。
小端存储模式:就是内存的低地址上存数据的低位,而高地址上存数据的高位。
用联合体,联合体也叫做共用体,它们共同占一个空间。
int check_sys()
{
union UN
{
char c;
int i;
}un;
un.i = 1;
if (un.c == 1)
return 0;//小端
else
return 1;//大端
}
先看一下代码:
class Person
{
public:
void Display()
{
cout << _id << "-" << _age << "-" << _heigh << endl;
}
void SetInfo(int id, int age, int heigh)
{
_id = id;
_age = age;
_heigh = heigh;
}
private:
int _id;
int _age;
int _heigh;
};
int main()
{
Person p1, p2;
p1.SetInfo(1, 18, 170);
p2.SetInfo(2, 19, 175);
p1.Display();
p2.Display();
return 0;
}
在上面代码的主函数中,我们分别实例化了两个对象p1和p2,也分别设置了他们的值,但是我们设置他们值,也就是调用他们的成员函数时,是怎么知道设置的哪一个对象呢,函数参数也没有对象的标识,这里面其实就是隐藏了一个参数,就是this指针
。谁调用成员函数,就把谁的地址传给this指针。
p1.SetInfo(1, 18, 170); // == SetInfo(&p1,1,18,170);
p1.Display(); //==Display(&p1);
而且这个隐藏的this指针是函数的第一个参数,在我们的成员函数定义中,每个函数的第一个参数都是含有this指针的
void Display() //==void Display(Person* const this)
{}
void SetInfo(int id, int age, int heigh)
{} //==void SetInfo(Person* const this,int id,int age,int heigh)
this指针
在成员函数中的参数格式为:类名* const this
之所以是const,是防止该之指针改变指向的对象,这是不合理的,加上了const才使得对象更加安全。
在我们定义的setInfo函数中,里面的成员也是this指针的指向。只是我们往往会忽略不写,为了区分成员属性和函数参数名,我们通常在成员属性名前加上_
,或者其他的标识符,用来区分他们的关系。
void SetInfo(int id, int age, int heigh)
{
_id = id; //==this->_id = id
_age = age;//==this->_age = age
_heigh = heigh;//==this->_heigh= heigh
}
但是并不是所有的成员函数都有this指针,只有在非静态的成员函数中才会有。且this指针不能在类外使用,只能在成员函数中使用。this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this 形参。所以对象中不存储this指针。我们的this指针属于函数形参,那就存放在栈上,但是在有些编译器中会进行优化,将this指针放到寄存器中(例如vs)。
this是一个指针,那他能不能是空指针呢?
答:可以为空,当我们调用函数时,如果函数内部不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串)。如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)就跟C中一样不能进行空指针的引用。