C++:类与对象(上)
- 类的引入
- 类的定义
- 访问限定符
- 类域
- 实例化
- 对象模型
- this指针
类的引入
C++的类是基于C语言的结构体优化出来的,那我们先来看一看C++对结构体有哪些优化点。
C语言与C++的结构体的类型名称略有区别,我们看一个案例:
//C语言的结构体
struct ListNode1
{
int val;
struct ListNode1* next;
};
//C++的结构体
struct ListNode2
{
int val;
ListNode2* next;
};
int main()
{
struct ListNode1 L1;//C语言的结构体创建变量
ListNode2 L2;//C++的结构体创建变量
}
一开始我们定义了两个结构体:ListNode1
和ListNode2
,在mian函数中我们分别对两个结构体创建了变量。
在C语言的结构体创建的变量时,需要用struct ListNode1
作为类型来创建。
而在C++的结构体创建变量时,只需要ListNode2
作为类型来创建。
也就是说,在C语言中结构体创建变量需要加上struct
关键字,而C++中优化了,可以无需这个关键字。
所以我们在结构体内部定义的next节点的指针,也分别是struct ListNode1*
和ListNode2*
。
C语言中结构体只能用于定义变量,但是在C++中,结构体内不仅可以定义变量,还可以定义函数。
struct stack
{
void Init()
{
cout << "初始化栈" << endl;
}
void Push(int x)
{
cout << x << "被压栈了" << endl;
}
void Destroy()
{
cout << "销毁栈" << endl;
}
int stackSize()
{
return size;
}
int* a;
int size;
int capacity;
};
上述代码中,我们定义了一个stack
结构体,其内部有a
,size
,capacity
三个基本变量,这是在C语言结构体范围内的。但是我们还额外定义了几个函数在其内部Init
,Push
,Destroy
以及stackSize
。我们可以通过结构体来调用某一个函数。
那么要如何调用函数?
其实只要把函数当作一个普通的变量,用.操作符访问即可。
这样我们就正常访问到了结构体内部的函数。
我们看到结构体内的一个函数:
int stackSize()
{
return size;
}
这个函数里面没有size
这个形参,为什么可以直接使用size
?
在同一个结构体内部的变量,是可以直接被结构体内的函数访问到的。因为size
存在于结构体中,所以结构体内的函数可以访问到size
。
但是为了更好的区别C语言的结构体与C++的结构体,C++更喜欢用一种叫做类的结构来替代结构体。
类的定义
class
关键字是定义类的关键字,在C++中,直接使用class
代替struct
即可,而我上方说的所有C++对结构体的优化,在类中也使用。
上述结构体可以定义为以下的类:
class stack
{
void Init()
{
cout << "初始化栈" << endl;
}
void Push(int x)
{
cout << x << "被压栈了" << endl;
}
void Destroy()
{
cout << "销毁栈" << endl;
}
int stackSize()
{
return size;
}
int* a;
int size;
int capacity;
};
其中函数部分叫做成员函数
或者方法
,变量部分叫做类的属性
或者成员变量
。
但是这样定义类会有一个问题:
为什么我们定义的类,无法访问到其内部的函数了???
这是因为对于类来说,其所有成员会被分为公有和私有,接下来我们看看什么是公有私有,以及如何转换:
访问限定符
public
修饰的成员在类外可以直接被访问protected
和private
修饰的成员在类外不能直接被访问(这方面protected
和private
是类似的)- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到} 即类结束。
class
的默认访问权限为private
,struct
为public
(因为struct
要兼容C语言)
首先看到第1,2点: public
修饰的成员,在类外部可以直接访问,而 protected
和private
修饰的成员在类外不能直接被访问。
再看到第五点: class
的默认访问权限为private
,struct
为public
。
也就是说,class定义的类,默认外部无法访问到其内部的成员,所以当我们把struct
转为 class
,外部访问函数就报错了。
所以以上代码要改为:
class stack
{
public:
void Init()
{
cout << "初始化栈" << endl;
}
void Push(int x)
{
cout << x << "被压栈了" << endl;
}
void Destroy()
{
cout << "销毁栈" << endl;
}
int stackSize()
{
return size;
}
int* a;
int size;
int capacity;
};
这样整个类都可以被外部访问了,因为所有的成员都在public
下方。
但是我们很多时候不希望别人修改我们的数据,只希望别人调用函数。
所以我们可以把成员变量
改为私有:
class stack
{
public:
void Init()
{
cout << "初始化栈" << endl;
}
void Push(int x)
{
cout << x << "被压栈了" << endl;
}
void Destroy()
{
cout << "销毁栈" << endl;
}
int stackSize()
{
return size;
}
private:
int* a;
int size;
int capacity;
};
对于protect
,这里不做详解,其与private
的区别要在后续才能体现。
类域
类是单独享有一个域的,所以类与类之间可以有同名函数与变量,如下:
class stack
{
public:
void Init()
{
cout << "初始化栈" << endl;
}
void Push(int x)
{
cout << x << "被压栈了" << endl;
}
void Destroy()
{
cout << "销毁栈" << endl;
}
int stackSize()
{
return size;
}
private:
int* a;
int size;
int capacity;
};
class List
{
public:
void Init()
{
cout << "初始化顺序表" << endl;
}
void Push(int x)
{
cout << x << "顺序表尾插" << endl;
}
void Destroy()
{
cout << "销毁顺序表" << endl;
}
private:
int* a;
int size;
};
以上代码中,定义了一个顺序表List
,一个栈stack
,两者都有Init
,Push
,Destroy
,a
,size
。但是两者并不冲突,因为在不同的类域中。
实例化
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
可以理解为,类就是一个建筑图纸,其规定了一个建筑有哪些房间,房间大小,尺寸等等。但是图纸是不可以住人的,也就是说还不是一个可以使用的屋子,此时就需要实例化,根据图纸把屋子建出来。
比如我们上述的栈的类,将其实例化就是:
int main()
{
stack s1;
stack s2;
stack s3;
stack s4;
return 0;
}
这里我们进行了四次实例化,s1
,s2
,s3
,s4
是四个不同的实例,不过我们一般将其称为对象。这四个对象就相当于根据图纸造出来的屋子,它们的结构完全一致,只是可能经过使用后数据会不同。可以理解为不同装修方式会对房屋的内部造成不同的影响,但是房屋的结构依然是一致的。
对象模型
我们先尝试检测一个对象的大小:
class stack
{
public:
void Init()
{
cout << "初始化栈" << endl;
}
void Push(int x)
{
cout << x << "被压栈了" << endl;
}
void Destroy()
{
cout << "销毁栈" << endl;
}
int stackSize()
{
return size;
}
private:
int* a;
int size;
int capacity;
};
int main()
{
cout << sizeof(stack) << endl;//输出16
return 0;
}
如果你结构体的位段学的好的话,你会发现,其实结构体中只有a
,size
,capacity
三个成员,大小也是16个字节。
也就是说:类中的函数没有占用类的空间。
其机制为:类的成员变量放在对象本身的空间中,而函数会被放在一个公共代码段,因为函数是一致的。
所以计算类的大小时,只需要通过结构体的位段,计算成员变量的大小。
除此之外:如果一个类是空类,或者只有成员函数,那么编译器依然会为其分配一个字节的空间。
this指针
讲清楚了对象模型后,我们来想一个问题:既然大家都是用的同一个函数,那么这个被调用的函数怎么知道是谁调用的?
比如这样:
class myclass
{
public:
void Init(int x, int y, int z)
{
_x = x;
_y = y;
_z = z;
}
void Add()
{
cout << _x + _y + _z << endl;
}
private:
int _x;
int _y;
int _z;
};
int main()
{
myclass c1;
myclass c2;
c1.Init(1, 2, 3);
c2.Init(4, 5, 6);
c1.Add();
c2.Add();
return 0;
}
创建了两个myclass
类的对象c1
和c2
,c1
初始化三个成员为1,2,3;c2
初始化三个成员为4,5,6。
调用Add函数时,对于Add
函数来说,它是如何知道要完成1+2+3
还是4+5+6
?
这就涉及到了this指针。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
也就是说,我们在调用类的函数时,函数会偷偷传递一个参数this
指针,它指向了成员的地址,这样就可以知道是谁调用了这个函数。
this指针有以下特性:
- this指针被const修饰了,函数内部是不允许修改this指针的指向的。
- this指针在函数内部是允许使用的
比如我们可以在类中添加一个函数:
void tellMeThis()
{
cout << this << endl;
}
这个函数会输出一个隐藏的变量this
,我们可以访问this
指针。
但是如果我们想在函数内部修改this:
void changeThis()
{
this = NULL;
}
这就是非法的了。