类与对象(上)
1. 类的引入
在C语言的结构体中,只能定义变量,但是在C++中给结构体进化成了类,它保留了在C中所有的用法,而且还可以再在结构体中定义函数,并且不用加struct定义变量,可以直接写类名。
struct Test
{
int a = 10;
int b = 20;
int Add(int a, int b)
{
return a + b;
}
};
int main()
{
Test test;
cout << test.Add(test.a, test.b) << endl;
return 0;
}
2.类的定义
但是在C++中更喜欢用class关键字来定义类
class className
{
//类体
};//分号别忘
类定义的两种方式
- 声明和定义全部放在类体中,注:如果以这种方式定义的话,类体中的函数默认是内联函数。
class Test
{
int a = 10;
int b = 20;
/*inline*/int Add(int a, int b)
{
return a + b;
}
};
- 声明和定义分离,(声明在.h,定义在.cpp),类体中的函数需要加上类名::
- 前面两种方式可以混用。
class Test
{
private:
int a = 10;
int b = 20;
public:
int Add(int a, int b);
void fun()
{
cout << "Hello World" << endl;
}
};
一般情况下,我们最好使用第三种定义方法,我们将代码少的成员函数定义和声明放在一起,代码量多的成员函数定义和声明分离。
成员变量名命名规则建议:
我们经常要初始化类的成员变量,会遇到下面的情况
class Test
{
private:
int year;
int month;
int day;
public:
void Init(int year, int month, int day)
{
//哪个是形参?哪个是成员变量名?
//这里是局部优先规则,都是形参
year = year;
month = month;
day = day;
}
};
所以我们建议在成员变量名前面加上一个_
class Test
{
private:
int _year;
int _month;
int _day;
public:
void Init(int year, int month, int day)
{
//哪个是形参?哪个是成员变量名?
//这里是局部优先规则,都是形参
_year = year;
_month = month;
_day = day;
}
};
3.类的访问限定符及封装
3.1 访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
说明:
- public修饰的成员变量,类外可以直接访问。
- protected 和 private修饰的成员变量,类外不能访问。
- 修饰限定的作用范围是从定义开始到下一个修饰限定符的出现。
- class定义的类和sturuct定义的类的区别是,class默认的访问限定符是private,struct默认的访问限定符是public。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
3.2 封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
4. 类的作用域
一个{}就代表一个域,类定义了一个域,类的所有成员都在该域里面,当类外定义成员的时候需要使用::来指明是哪个作用域,就像我们上面所说的,当成员函数的定义和声明分离的时候就用到了::
5. 类的实例化
函数的声明和定义很好区分,但是变量的定义和声明如何区分呢?那就是看有没有给这个变量分配空间。
class Test
{
public:
int year;
int month;
int day;
};
int main()
{
//这里的year只是声明,没有空间,所以这种写法是错误的
Test::year = 1;
return 0;
}
总结:
- 定义一个类,并没有分配一个空间来存储它,而类的实例话就是给类分配一个空间,创造出一个对象。
- 一个类可以实例化很多的对象,就像一张图纸可以构造出很多的房子。但这些对象的基本属性和行为特征都相似。
class Test
{
public:
int year;
int month;
int day;
};
int main()
{
Test test1;
Test test2;
return 0;
}
6. 类的对象模型
6.1如何计算类对象的大小
类里既有成员变量又有成员函数,空间是如何分配的?类对象的大小如何计算?
class Test
{
public:
int year;
int month;
int day;
};
int main()
{
Test test1;
cout << sizeof(Test) << endl;
return 0;
}
通过内存对齐的方式我们可以计算到这个结果,但如果有成员函数呢?
class Test
{
public:
int year;
int month;
int day;
void fun()
{
cout << year << endl;
}
};
int main()
{
Test test1;
cout << sizeof(Test) << endl;
return 0;
}
可以看出大小一样,所以成员函数不在类对象里,计算类对象的大小的时候只需要考虑成员变量和内存对齐规则,不需要考虑成员函数。
6.2 类对象的存储方式
对于类对象的存储方式我们有三种猜测
- 对象中包含所有的成员(变量和函数) 这种方式有很大的缺陷,每个对象的成员变量不同,但是调用同一个函数,如果按此种方式存储,那每个对象都保存多次相同的代码段,浪费空间。
- 对象中包含成员变量和成员函数地址的指针 函数代码只有一份,对象中保存函数地址。
- 对象中只包含成员变量,成员函数在公共代码段。
那么这三种方式,计算机是按照哪种方式存储的呢?根据6.1我们知道,计算机是按照第三种方式存储的,但是空类(没有成员变量)比较特殊,编译器给了空类一个字节大小来标识自己。
class Test
{
};
int main()
{
Test test1;
cout << sizeof(Test) << endl;
return 0;
}
6.3 结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
7. this指针
7.1 this指针的引出
我们先看一段代码
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 main()
{
Date date1;
Date date2;
date1.Init(2022, 10, 20);
date2.Init(2023, 10, 20);
date1.Print();
date2.Print();
return 0;
}
根据上面讲的类对象的存储方式,可知我们调用的公共代码段的同一函数,函数中没有关于不同对象的区分,当date1调用函数,该函数是如何区分是打印date1的数据还是date2的数据?
C++通过引入this指针来解决了这个问题,就是C++编译器给非静态函数都默认隐藏的增加了一个指针,指向调用该函数的对象。但是这些对用户都是透明的,不需要用户自己来完成,编译器自动完成。
//编译器处理后的代码
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print(Data* const this)
{
cout << this->_year <<"-"<< this->_month<<"-" << this->_day<< endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date date1;
Date date2;
date1.Init(2022, 10, 20);
date2.Init(2023, 10, 20);
date1.Print(&date1);
date2.Print(&date2);
return 0;
}
7.2 this指针的特性
- 该指针的类型*const类型,即不能给this指针赋值。
- 只能在成员函数内部使用
- this是形参,并不存储在类中。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递
经典面试题
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
//解析:该代码最终是正常运行的,因为this指针虽然是空的,但是在Print()函数中没有调用
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
//解析:该代码最终是运行崩溃,因为空指针访问
所以this指针是可以为空的。