C++ 类 & 对象
C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。
**类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。
** 类中的数据称为类的成员变量,函数在一个类中被称为类的成员函数。
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
double getVolume(void)
{
return length * breadth * height;
}
};
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
double getVolume(void);
};
//您也可以在类的外部使用范围解析运算符 :: 定义该函数
double Box::getVolume(void)
{
return length * breadth * height;
}
类的访问修饰符
数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。
一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。
class Base {
public:
// 公有成员
protected:
// 受保护成员
private:
// 私有成员
};
类的访问属性
访问属性 | 外部访问 | 子类访问 | 类内访问/友元函数/友类 |
public | Yes | Yes | Yes |
protected | No | Yes | Yes |
private | No | No | Yes |
- 默认情况下,没有使用任何访问修饰符的成员是私有的。只有类和友元函数可以访问私有成员。
类的继承方式
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
1.public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private。public继承保留基类的访问权限。
2.protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private。protected继承
3.private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private
但无论哪种继承方式,上面两点都没有改变:
1.private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
2.protected 成员可以被派生类访问。
class A { }
class B : public A{ }
class C : protected A{ }
class D : private A{ }
继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化概括 |
public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在子类的访问属性不变 |
protected继承 | 变为protected成员 | 变为protected成员 | 不可见 | 基类的非私有成员都为子类的保护成员 |
private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类中的非私有成员都称为子类的私有成员 |
构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,不返回void或任何类型。构造函数可用于为某些成员变量设置初始值。
不带参数的构造函数
class Line
{
public:
Line(); // 这是构造函数
};
Line::Line(void)
{
cout << "Object is being created" << endl;
}
int main( )
{
Line line;
return 0;
}
>>> Object is being created
带参数的构造函数
构造函数在类被创建时执行,不具有接口功能,用作初始化类对象。这就需要传入参数,来初始化实例。
初始化有两种方法:
- 使用初始化列表来初始化字段。
- 在构造函数体内初始化字段。
class student{
string name;
int age;
public:
student(string n,int a):name(n),age(a){}
//以上代码等同于
student(string n,int a){
name=n;
age=a;
}
}
析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
不同于带参数的构造函数,析构函数不需要主动调用,跳出程序时会被自动调用。我们可以自定义析构函数执行时输出内容
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line; //这里执行构造函数
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
//这里执行析构函数
return 0;
}
>>>Object is being created
>>>Length of line : 6
>>>Object is being deleted
拷贝构造函数
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。当类带有指针变量,并有动态内存分配,拷贝构造函数是必须的。拷贝构造函数的最常见形式如下:
classname (const classname &obj) {
// 构造函数的主体
}
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象。
Line line2 = line1;
- 复制对象把它作为参数传递给函数。
display(line1);
- 复制对象,并从函数返回这个对象。
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}
int main( )
{
Line line1(10);
Line line2 = line1; // 这里也调用了拷贝构造函数
display(line1);
display(line2);
return 0;
}
>>>
调用构造函数// Line line1(10);
调用拷贝构造函数并为指针 ptr 分配内存//Line line2 = line1;
调用拷贝构造函数并为指针 ptr 分配内存// display(line1);
line 大小 : 10// display(line1);
释放内存// display(line1);
调用拷贝构造函数并为指针 ptr 分配内存// display(line2);
line 大小 : 10// display(line2);
释放内存// display(line2);
释放内存//line2
释放内存//line1
友元
类的友元定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。友元有两种表现形式:
- 友元函数
友元函数的原型在类的定义中出现过,但是友元函数并不是成员函数。
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。
class Box
{
double width;
public:
double length;
//友元函数
friend void printWidth( Box box );
void setWidth( double wid );
};
// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
//友元可以直接访问该类的私有成员
cout << "Width of box : " << box.width <<endl;
}
- 友元类
被声明的类可以访问当前类的所有成员。
声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:
friend class ClassTwo;
请看下面这段代码
#include <iostream>
using namespace std;
class Shape{
double* area=NULL;
friend class user;
public:
Shape(double a){
area=new double;
*area=a;
cout<<"Shape 已经创建,area:"<<*area<<endl;
}
~Shape(){cout<<"Shape 已经销毁"<<endl;}
Shape(const Shape &obj){
area=new double;
*area=*obj.area;
cout<<"拷贝构造函数创建,赋值为"<<*area<<endl;
}
};
class user{
string name;
public:
user(string n):name(n){
cout<<"user 已经创建,name:"<<name<<endl;
}
~user(){
cout<<"user 已经销毁"<<endl;
}
void show(Shape S){
cout<<"面积为:"<<*S.area<<endl;
cout<<"user为:"<<name<<endl;
}
};
int main(){
Shape myshape(324);
user kbl("kbl");
kbl.show(myshape);
return 0;
}
运行结果:
Shape 已经创建,area:324
user 已经创建,name:李博凌
拷贝构造函数创建,赋值为324
面积为:324
user为:李博凌
Shape 已经销毁
user 已经销毁
Shape 已经销毁
在user类中做小小的改动,你能看出什么不一样么?
class user{
string name;
public:
user(string n):name(n){
cout<<"user 已经创建,name:"<<name<<endl;
}
~user(){
cout<<"user 已经销毁"<<endl;
}
void show(Shape &S){
cout<<"面积为:"<<*S.area<<endl;
cout<<"user为:"<<name<<endl;
}
};
输出为:
Shape 已经创建,area:324
user 已经创建,name:kbl
面积为:324
user为:kbl
user 已经销毁
Shape 已经销毁
如果传入的是引用并不会调用拷贝构造函数,原因也显然,引用传入的是原对象的地址,并没有拷贝一个同样的对象再传入。
这段代码还需要注意下面这段:
double* area=NULL;
Shape(double a){
area=new double;
*area=a;
cout<<"Shape 已经创建,area:"<<*area<<endl;
}
Shape(const Shape &obj){
area=new double;
*area=*obj.area;
cout<<"拷贝构造函数创建,赋值为"<<*area<<endl;
}
对于初始化为NULL的指针,使用时要分配地址。如果不分配地址会怎样?
将程序改为需要调用拷贝构造函数的形式。为了方便演示,只把拷贝构造函数的内存分配注释掉,构造函数仍然正常运行。
Shape(double a){
area=new double;
*area=a;
cout<<"Shape 已经创建,area:"<<*area<<endl;
}
Shape(const Shape &obj){
//area=new double;
*area=*obj.area;
cout<<"拷贝构造函数创建,赋值为"<<*area<<endl;
}
void show(Shape S){
cout<<"面积为:"<<*S.area<<endl;
cout<<"user为:"<<name<<endl;
}
int main(){
Shape myshape(324);
user kbl("kbl");
kbl.show(myshape);//程序在这里中断了,并且没有报错。
return 0;
}
输出结果:
Shape 已经创建,area:324
user 已经创建,name:kbl
指向NULL即地址0的指针,访问了指向用户态地址空间,这是不可访问的。是典型的Segmentation fault错误。
内联函数
函数的世界是残酷的——— 一个函数在被另一个函数调用的时候,才有生命,才会为其准备对应的内存空间,再调用完毕之后再清理释放结束。
每一次的函数调用都会带来一些时间和空间上的花销。那么我们考虑,一个代码本身就不长,甚至只有一行,比如只包含判断,但是它频繁被调用,需要反复的分配空间,清理释放(内存抖动?),这样做是不是显然不必要了呢?
对于这种函数,我们把它定义为内联的,在函数前加上 inline 。
inline int Max(int a,int b)
{
return a>b?a:b;
}
如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。这就导致对内联函数进行任何修改,都需要重新编译函数的所有客户端。编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。引入内联函数的目的是为了解决程序中函数调用的效率问题。内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。是空间代价换时间的方法。
使用内联函数我们需要注意下面四点:
- 内联函数的定义要在调用之前出现,才可以让编译器在编译期间了解上下文,进行代码替换.
- 内联函数与register变量类似,仅仅是我们提给编译器的一个请求,最终是否真正会实现内联。
- 在内联函数内不允许使用循环语句和switch语句。
- 只有当函数只有 10 行甚至更少时才将其定义为内联函数。