类和对象
类和结构体一样的,是个自定义的数据类型,同时类和结构体是很像的,包括内存大小等性质都很像。
(1)类的定义
class Box {
protected: // 访问修饰符
double length = 1.0;
double breedth = 1.0;
double height = 1.0;
public:
double get();
void set(double len, double bre, double hei);
}; // 类的定义结束后有分号
double Box::get() { // 注意返回值
return length * breedth *
height; // 写成return Box.length * Box.breedth * Box.height 会报错, 因为.号前面只能是对象名,不是类名
}
void Box::set(double len, double bre, double hei) {
length = len;
breedth = bre;
height = hei;
}
(2)::
和.
的区别
::
是类操作符,它的前面一定是个类的名字,.
是对象操作符,它的前面一定是个对象名字。
(3)类的访问修饰符public
,protected
,private
关键字 public
,protected
,private
称为访问修饰符。一个类可以有多个 public
、protected
或 private
标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右花括号之前都是有效的。成员和类的默认访问修饰符是 private
。
<1> public
成员在类的外部是可访问的,可以不使用任何成员函数来直接修改和获取(访问)公有变量的值。
<2> private
成员变量或成员函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员,类的成员函数可以修改私有成员,但是private成员在派生类(子类)中是不可访问的,也不可修改。实际操作中,一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数,即可以通过公有区域内的函数来访问或者修改私有区域的数据。
<3> protected
成员变量或成员函数与 private
成员十分相似,也可以通过类和友元函数访问,可以通过类的成员函数修改私有成员。但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的,但是是不可被修改的,private成员在派生类(即子类)中却不可访问。
#include <iostream>
using namespace std;
class Box {
protected: // protected 成员可以被子类访问,但是不可被子类修改
// private:
double length = 1.0;
double breedth = 1.0;
double height = 1.0;
public:
double get();
void set(double len, double bre, double hei);
}; // 类的定义结束后有分号
double Box::get() { // 注意有返回值
return length * breedth * height;
}
void Box::set(double len, double bre, double hei) {
length = len;
breedth = bre;
height = hei;
}
class A : public Box { // 公有继承
public:
double get();
};
double A::get() {
return length * breedth * height;
}
int main() {
A box1;
// box1.height = 5.0; // 虽然 box1 是继承的子类 A 的对象,可访问但是却不可直接修改
// box1.breedth = 4.0;
// box1.length = 3.0;
cout << box1.get() << endl;
cout << box2.get() << endl;
}
综上:private成员和protected成员很相似,都可以通过类的成员函数修改和访问,只是private成员在派生类(子类)中不可访问,而protected成员在派生类中可以访问。
(2)类的成员函数
<1> 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员,包括private
成员和protexted
成员。
<2> 定义在类中的成员函数缺省都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外定义函数的时候要加上 inline
,否则就认为不是内联的。关键字 inline
必须与函数定义体
放在一起才能使函数成为内联,仅将 inline
放在函数声明前面不起任何作用。
inline void Foo(int x, int y); // inline 仅与函数声明放在一起,不能成为内联函数
void Foo(int x, int y){}
void Foo(int x, int y);
inline void Foo(int x, int y) {} // inline 与函数定义体放在一起,成为内联函数
(3)类的构造函数
<1>构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void
。 构造函数可用于为某些成员变量设置初始值。
#include <iostream>
using namespace std;
class Line {
public:
Line(); // 这是构造函数
};
Line::Line() { // 类的构造函数定义
cout << "Object is being created" << endl;
}
int main( ) {
Line line; // 创造对象时就会调用构造函数
return 0;
}
>>>
Object is being created
<2> 带参数的构造函数
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。
#include <iostream>
using namespace std;
class Line {
public:
int length = 0;
Line(double len); // 构造函数
};
Line::Line( double len) { // 成员函数定义,包括构造函数
length = len;
cout << "Object is being created, length = " << length << endl;
}
int main( ) {
Line line(10.0);
return 0;
}
>>>
Object is being created, length = 10
<3>使用初始化列表来初始化字段
Line::Line( double len): length(len){
cout << "Object is being created, length = " << lenth << endl;
}
上面代码相当于
Line::Line( double len) { // 成员函数定义,包括构造函数
length = len;
cout << "Object is being created, length = " << length << endl;
}
假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:
C::C(double a, double b, double c): X(a), Y(b), Z(c){
....
}
c++初始化类成员时,需要按照声明的顺序初始化。比如下面的例子:
class CMyClass { // C++ 初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
CMyClass(int x, int y); // 构造函数
int m_x;
int m_y;
};
CMyClass::CMyClass(int x, int y) : m_y(y), m_x(m_y) // 初始化列表
你可能以为上面的代码将会首先做 m_y= y,然后做 m_x = m_y,最后它们有相同的值。但是编译器先初始化 m_x,然后是 m_y,,因为它们是按这样的顺序声明的。结果是 m_x 将有一个不可预测的值。有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。
<4> 一个类内可以有多个构造函数,可以是一般类型的,也可以是带参数的,相当于重载构造函数,但是析构函数只能有一个。
class Matrix{
public:
Matrix(int row, int col); //普通构造函数
Matrix(const Matrix& matrix); //拷贝构造函数
Matrix(); //构造空矩阵的构造函数
void print(void);
~Matrix(); // 析构函数
};
(4)c++拷贝构造函数 c++拷贝构造函数详解
<1>对于普通类型的变量来说,它们之间的复制是很简单的。
int a = 100;
int b = a;
但是类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面是一个类对象拷贝的例子。
#include<iostream>
using namespace std;
class CExample{
private:
int a;
public:
//构造函数
CExample(int b){ // 构造函数是为本对象进行初始化的
a=b;
printf("constructor is called\n");
}
//拷贝构造函数
CExample(const CExample& c){ // 拷贝构造函数是为了给其他对象初始化用的
a=c.a;
printf("copy constructor is called\n");
}
//析构函数
~CExample(){
cout<<"destructor is called\n";
}
void Show(){
cout<<a<<endl;
}
};
int main(){
CExample A(100); //
// CExample B=A;
A.Show();
return 0;
}
>>>
constructor is called
100
destructor is called
=================================================================
int main(){
CExample A(100); // 初始化对象会调用构造函数
CExample B=A; // 只有出现了对象间的复制时就会调用拷贝构造函数
B.Show();
return 0;
}
>>>
constructor is called // 这个构造函数是用来初始化 A 对象的
copy constructor is called=========== // 这个拷贝构造函数时用来初始化 B 对象的
100
destructor is called // 这个析构函数是用来销毁 B 对象的
destructor is called // 这个析构函数是用来销毁 A 对象的
上面的代码中系统为对象 B 分配了内存并完成了与对象 A 的复制过程。这个复制过程是通过拷贝构造函数CExample(const CExample& C)
来完成的,如果我们没有自定义拷贝构造函数,系统会自行定义一个。可见,**拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
**,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象。
- 复制对象把它作为参数传递给函数。
- 复制对象,并从函数返回这个对象。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
<2>拷贝构造函数的调用时机 - 当函数的参数为类的对象时
#include<iostream>
using namespace std;
class CExample{
private:
int a;
public:
CExample(int b){
a=b;
printf("constructor is called\n");
}
CExample(const CExample & c){
a=c.a;
printf("copy constructor is called\n");
}
~CExample(){
cout<<"destructor is called\n";
}
void Show(){
cout<<a<<endl;
}
};
void g_fun(CExample c){
cout<<"g_func"<<endl;
}
int main(){
g_fun(A);
return 0;
}
调用g_fun()时,会产生以下几个重要步骤:A对象传入形参时,会先会产生一个临时对象 C —> 然后调用拷贝构造函数把 A 的值给 C。 整个这两个步骤有点像:CExample C(A) —> 等 g_fun() 执行完后, 析构掉 C 对象。
<2>函数返回值是类的对象
CExample g_fun()
{
CExample temp(0);
return temp;
}
当g_Fun()函数执行到return时,会产生以下几个重要步骤:先会产生一个临时变量,就叫XXXX吧 —> 然后调用拷贝构造函数把temp的值给XXXX。整个这两个步骤有点像:CExample XXXX(temp) —> 在函数执行到最后先析构temp局部变量 —> 等g_fun()执行完后再析构掉XXXX对象。
<3>对象需要通过另外一个对象进行初始化
CExample A(100);
CExample B=A;
<3>拷贝构造函数的几个细节
-拷贝构造函数里能调用private成员变量吗?
拷贝构造函数其实就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。
- 以下函数哪个是拷贝构造函数,为什么?
X::X(const X&); // yes
X::X(X); // no
X::X(X&, int a=1); // no
X::X(X&, int a=1, int b=2); // no
解答:对于一个类 X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
- 一个类中可以存在多于一个的拷贝构造函数吗?
类中可以存在超过一个拷贝构造函数。也可以存在多个构造函数 - 构造函数是可以有参数的,拷贝构造函数参数是相同类型对象的引用。析构函数是没有参数的。如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
深拷贝和浅拷贝
引申:很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:
Rect::Rect(const Rect& r) // 这个代码不用我们编写,编译器会为我们自动生成。
{
width=r.width;
height=r.height;
}
但是这种系统自动生成的默认拷贝构造函数并不能实现所有情况下的对象拷贝。
#include<iostream>
using namespace std;
class Rect{ // 使用了默认的拷贝构造函数
public:
Rect(){ // 构造函数,每次构造一个 Rect 类的实例对象时count就会 +1
count++;
}
~Rect(){ // 析构函数,析构一个 Rect 类的实例对象时 count 就会 -1
count--;
}
static int getCount(){ // 静态成员函数
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main(){
Rect rect1;
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
return 0;
}
>>>
The count of Rect:1
The count of Rect:1
这段代码中的类加入了一个静态成员,目的是对类的实例对象个数进行计数。在主函数中,首先创建对象 rect1,输出此时的对象个数,然后使用 rect1 复制出对象 rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。这说明系统生成的默认拷贝构造函数没有处理静态数据成员。出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:
#include<iostream>
using namespace std;
class Rect{
public:
Rect(){
count++;
}
Rect(const Rect& r){
width=r.width;
height=r.height;
count++;
}
~Rect(){
count--;
}
static int getCount(){
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0; // 静态成员数据只能在类外定义
int main(){
Rect rect1;
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
return 0;
}
>>>
The count of Rect:1
The count of Rect:2
- 浅拷贝
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的就是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员(动态内存分配),那么浅拷贝就会出问题了,比如下面代码:
#include<iostream>
#include<assert.h>
using namespace std;
class Rect{
public:
Rect(){
p=new int(100); // 构造函数里有一个内存动态分配的语句,new分配的内存一定在堆上
}
~Rect(){
assert(p!=NULL);
delete p;
}
private:
int width;
int height;
int *p;
};
int main(){
Rect rect1;
Rect rect2(rect1);
return 0;
}
在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作,因此执行后的内存情况大致如下。
在使用 rect1 复制 rect2 时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时rect1.p = rect2.p
,也即这两个指针指向了堆里的同一个空间,如下图所示:
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
- 深拷贝
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:
class Rect{
public:
Rect(){ // 构造函数,p指向堆中分配的一空间
p = new int(100);
}
Rect(const Rect& r){
width = r.width;
height = r.height;
p = new int; // 为新对象重新动态分配空间
*p = *(r.p);
}
~Rect(){ // 析构函数,释放动态分配的空间
if(p != NULL){
delete p;
}
}
private:
int width;
int height;
int *p; // 一指针成员
};
此时,在完成对象的复制后,内存的一个大致情况如下:
此时 rect1 的 p 和 rect2 的 p 各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。
- 防止默认拷贝的发生
通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,有一个小技巧可以防止按值传递-----声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
// 防止按值传递
class CExample{
private:
int a;
public:
CExample(int b){ //构造函数
a = b;
cout<<"creat: "<<a<<endl;
}
private:
CExample(const CExample& C); // 拷贝构造,只要把默认拷贝构造函数声明成private即可
public:
~CExample(){ // 析构函数
cout<< "delete: "<<a<<endl;
}
void Show (){
cout<<a<<endl;
}
};
//全局函数
void g_Fun(CExample C){
cout<<"test"<<endl;
}
int main(){
CExample test(1);
//g_Fun(test); 按值传递将出错
return 0;
}
(5)类的析构函数
#include <iostream>
using namespace std;
class Line {
public:
int length = 0;
Line(int length); // 构造函数声明,构造函数可以带参数
~Line(); // 析构函数声明,析构函数没有参数
};
Line::Line(int len) {
length = len;
cout << "Object is being created! length =" << length << endl;
}
Line::~Line() {
cout << "Object is being deleted!" << endl;
}
int main() {
Line line(6.0);
return 0;
}
>>>
Object is being created! length =6
Object is being deleted!
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。与构造函数一样,析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数(构造函数可以带参数)。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。析构时先执行析构函数中的语句(此时成员还都在),再具体析构对象成员,且顺序和构造时相反,即后声明的对象先析构。
(6)类的友元函数
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。声明方式如下:
class Box{
double width;
public:
double length;
friend void printWidth( Box box ); // 友元函数需要在类内声明一下
friend class Iron_Box; // 声明类 Iron_Box 的所有成员函数作为类 Box 的友元
void setWidth( double wid );
};
(7)类的内联函数
引入内联函数的目的是为了解决程序中函数调用的效率问题。程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代(也就是会出现中断压栈的操作)。内联函数其实就是空间代价换时间的节省。所以内联函数一般都是1-5行的小函数。
- 在内联函数内不允许使用循环语句和开关语句;
- 内联函数的定义必须出现在内联函数第一次调用之前;
- 类结构中所在的类说明内部定义的函数是内联函数
(8)c++ this 指针
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址,类和结构体很相似,结构体也有this指针。this 指针是所有成员函数(或成员变量)的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。因为 this 的目的总是指向“这个”对象,所以 this 是一个指针常量,我们不允许改变 this 中保存的地址。
#include <iostream>
using namespace std;
class Box{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0){ // 带参数的构造函数
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume(){
return length * breadth * height;
}
int compare(Box box){
return this->Volume() > box.Volume();
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void){
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
if(Box1.compare(Box2)){
cout << "Box2 is smaller than Box1" <<endl;
}
else{
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}
>>>
Constructor called.
Constructor called.
Box2 is equal to or larger than Box1
下面的例子,this 指针的类型可理解为 Box* (this指针就是个特殊的指向类的指针)。此时得到两个地址分别为 box1 和 box2 对象的地址。
#include <iostream>
using namespace std;
class Box{
public:
Box(){;}
~Box(){;}
Box* get_address() //得到this的地址
{
return this;
}
};
int main(){
Box box1;
Box box2;
// Box* 定义指针p接受对象box的get_address()成员函数的返回值,并打印
Box* p = box1.get_address();
cout << p << endl;
p = box2.get_address();
cout << p << endl;
return 0;
}
(9)指向类的指针
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->
,就像访问指向结构的指针一样。与所有的指针一样,在使用指针之前,对指针进行初始化。
#include <iostream>
using namespace std;
class Box{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0){
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume(){
return length * breadth * height;
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void){
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
Box *ptrBox; // Declare pointer to a class.
// 保存第一个对象的地址
ptrBox = &Box1;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box1: " << ptrBox->Volume() << endl;
// 保存第二个对象的地址
ptrBox = &Box2;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box2: " << ptrBox->Volume() << endl;
return 0;
}
>>>
Constructor called.
Constructor called.
Volume of Box1: 5.94
Volume of Box2: 102
(9)c++类的静态成员(类内的 static
用法)
<1>static
关键字也叫存储类,可以用来修饰局部变量。本来局部变量在作用域执行完成之后就会销毁。用static修饰局部变量时,如static int i = 5,作用为指示编译器在程序(整个主程序)的生命周期内保持局部变量的存在。static局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。循环里面可以用static保持局部变量,防止跳出循环后值改变。
<2> static 修饰全局变量时,会使全局变量的作用域限制在声明它的文件内。
<3> 使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 ::
来重新声明静态变量从而对它进行初始化。
- 静态成员变量
#include <iostream>
using namespace std;
class Box
{
public:
static int objectCount; // 不能再类内初始化静态成员数据
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0; // 静态成员数据的初始化
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 输出对象的总数
cout << "Total objects: " << Box::objectCount << endl;
return 0;
}
- 静态成员函数
把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符::
就可以访问。静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。静态成员函数有一个类范围,他们不能访问类的this
指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。静态成员函数与普通成员函数的区别: - 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
- 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。
类的继承(类与类直接的关系)
继承有 public
, protected
, private
三种方式,它们相应地改变了基类成员的访问属性。
(1)三种继承方式
如果继承时不显示声明是 private,protected,public 继承,则默认是 private 继承,在 struct 中默认 public 继承。
<1> public继承
:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
<2>protected 继承
:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
<3>private 继承
:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private
但无论哪种继承方式,上面两点都没有改变:
- private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
-protected 成员可以被派生类访问。
(2)多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。C++ 类可以从多个类继承成员。
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost // 多继承语法
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}
类和对象的关系、类与类之间的关系
类可以看作是 c 语言中结构体的升级版,都是用户定义的数据类型。
(1)类与对象之间的关系:类定义了一个模板,对象是类的实例化,类只有通过对象才能起作用。
(2)类与类之间的关系:类与类之间的关系主要有:派生(也叫继承)。
类和对象的内存大小 类和对象的内存大小
(1)类所占内存的大小是由成员变量(静态变量除外)决定的,成员函数是不计算在内的。
#include <iostream>
using namespace std;
class Box {
protected:
double length = 1.0;
double breedth = 1.0;
double height = 1.0;
public:
double get();
void set(double len, double bre, double hei);
}; // 类的定义结束后有分号
double Box::get() { // 注意有返回值
return length * breedth *
height; // 写成return Box.length * Box.breedth * Box.height 会报错, 因为.号前面只能是对象名,不是类名
}
void Box::set(double len, double bre, double hei) {
length = len;
breedth = bre;
height = hei;
}
int main() {
Box box1, box2, box3; // box1, box2, box3都是对象,不是Box的子类
cout << sizeof(double) << endl; // 8
cout << sizeof(Box) << endl; // 24, 三个double类型的数就占 24 个字节
cout << sizeof(box1) << endl; // 24
cout << box1.get() << endl; // 1
}
>>>
8
24
24
1
(2)空类占 1 个字节内存,因为 c++ 要求每个实例在内存中都有独一无二的地址。 空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的 sizeof 为 1。
class CBase { // 空类占 1 个字节。c++要求每个实例在内存中都有独一无二的地址。
};
cout << sizeof(CBase) << endl;
>>>
1
(3)类的内存有对齐原则,这点和结构体的对齐原则很像
class CBase { //
int a; // 4 字节
char p; // char 占 1 字节,补充 3 字节
};
cout << sizeof(CBase) << endl;
>>>
8
(4)类的内存是不计算类的静态成员的
#include <iostream>
using namespace std;
class Box {
protected:
double length = 1.0;
double breedth = 1.0;
double height = 1.0;
public:
double get();
void set(double len, double bre, double hei);
static int a;
};
int Box::a = 1; //
int main() {
Box box1, box2, box3; // box1,box2,box3都是对象,不是Box的子类
cout << sizeof(double) << endl;
cout << sizeof(Box) << endl; // 24
cout << sizeof(box1) << endl; // 24
cout << box1.get() << endl;
cout << box2.get() << endl;
}
>>>
24 // 静态成员数据没有被计算在内
(5)类内有虚函数时的内存大小 虚函数内存大小
类内如果有多个虚函数,最后都会保存在一个虚函数表中,在类中不管有多少个虚函数,最后都会给一个虚函数表指针(也就是64bit系统占8个字节地址)。其他数据的偏移会有变化。
#include <iostream>
using namespace std;
class Box {
protected:
double length = 1.0;
double breedth = 1.0;
double height = 1.0;
public:
double get();
void set(double len, double bre, double hei);
virtual double aaa(double len, double bre);
virtual double bbb(double len, double bre);
static int a;
};
int Box::a = 1;
int main() {
Box box1, box2, box3; // box1,box2,box3都是对象,不是Box的子类
cout << sizeof(double) << endl;
cout << sizeof(Box) << endl; // 32
cout << sizeof(box1) << endl; // 32
}
>>>
8
32
32
C++重载运算符和重载函数
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。重载是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
(1)c++的函数重载
在同一个作用域内声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数。
#include <iostream>
using namespace std;
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}
void print(double f) {
cout << "浮点数为: " << f << endl;
}
void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};
int main(void)
{
printData pd;
// 输出整数
pd.print(5);
// 输出浮点数
pd.print(500.263);
// 输出字符串
char c[] = "Hello C++";
pd.print(c);
return 0;
}
>>>
整数为: 5
浮点数为: 500.263
字符串为: Hello C++
(2)C++ 中的运算符重载
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。大多数的运算符重载(函数)被定义为普通的非成员函数或者定义为类的成员函数,这两种定义有一点区别:
<1>如果运算符重载函数被定义为普通的非成员函数,那么函数的定义一般如下:
Box operator+(const Box&, const Box&); // 需要为每次操作传递两个参数
<2>如果运算符重载函数被定义为类成员函数,那么利用this指针
只需要传入一个参数即可。
Box operator+(const Box&);
下面的例子说明了运算符重载的概念。对象作为参数进行传递,对象的属性使用 this 运算符
进行访问
#include <iostream>
using namespace std;
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 把体积存储在该变量中
// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;
// Box2 的体积
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;
// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的体积
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;
return 0;
}
>>>
Volume of Box1 : 210
Volume of Box2 : 1560
Volume of Box3 : 5400
(3)运算符重载的性质
- 运算重载符不可以改变语法结构。
- 运算重载符不可以改变操作数的个数。
- 运算重载符不可以改变优先级。
- 运算重载符不可以改变结合性。
c++多态、虚函数
c++多态
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。下面的例子:
#include <iostream>
using namespace std;
class Shape { // Shape派生除了两个类
protected:
int width, height;
public:
Shape( int a=0, int b=0){
width = a;
height = b;
}
int area(){
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) {}
int area (){
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) {}
int area (){
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( ){
Shape *shape; // shape
Rectangle rec(10,7); // 一个对象
Triangle tri(10,5); // 另一个对象
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
>>>
Parent class area :
Parent class area :
上面这个代码输出的结果不是我们想要的,导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接(函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。),此时需要在基类的 area() 函数声明前加关键字 vurtual
。
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0){
width = a;
height = b;
}
virtual int area(){
cout << "Parent class area :" <<endl;
return 0;
}
};
>>>
Rectangle class area :
Triangle class area :
此时,编译器看的是指针的内容,而不是指针的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape
中,所以会调用各自的 area()
函数。
虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数.
上面的例子中,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,可以有多个不同的类,都带有同一个名称但具有不同实现的成员函数,函数的参数甚至可以是相同的。
c++虚函数:用来完成多态操作时的 运行时决议 这一操作,虚函数是实现多态(动态绑定/接口函数的基础)
c++虚函数详细解释,内存大小等
虚函数的详解
虚函数是在基类中使用关键字 virtual
声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
c++纯虚函数
我们想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。可以把基类中的虚函数 area() 改写如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0){
width = a;
height = b;
}
virtual int area() = 0; // =0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数
};
C++中函数调用非虚成员函数、调用虚函数的区别:
1.调用非虚成员函数:和调用非成员函数一样,通过对象确定对象所属的类,然后找到类的成员函数。此过程不会涉及到对象的内容,只会涉及对象的类型,是一种静态绑定。
2.调用虚函数与调用非虚成员函数不同,需同过虚函数表找到虚函数的地址,而虚函数表存放在每个对象中,不能再编译期间实现。只能在运行时绑定,是一种动态绑定。