C++学习笔记(三)——类与对象
本此文章讲解以下5点:
- 如何创建一个C++类;
- 类的实例化;
- 类的构造函数与初始化表达式;
- 类的析构函数;
- 类的复制构造函数;
如何创建一个C++类
类声明代码如下:
class name
{
public:
...
private:
...
protected:
...
};
其中的public,private,protected
关键字的作用就是限定其下元素与方法的访问权限。总结如下:
成员变量修饰符 | 类外普通函数 | public派生类 | protected派生类 | private派生类 |
---|---|---|---|---|
public | 可以访问 | 可以访问 | 可以访问 | 可以访问 |
protected | 可以访问 | 不可以访问 | 不可以访问 | 不可以访问 |
private | 不可以访问 | 不可以访问 | 不可以访问 | 不可以访问 |
类的方法(函数)的定义有两种方法:
- 类内实现类函数:
示例代码如下(代码3-0):
///
/// @file Computer.cc
/// @date 2019-02-06 14:23:56
///
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Computer
{
private://private表现的是封装特性
char m_brand[20];//私有成员的命名规范:_name;m_name;name_
float _fprice;
public:
void print()
{
cout << "品牌: " << m_brand << endl;
cout << "价格: " << _fprice << endl;
}
void setBrand(const char * brand)
//通过setBrand来修改成员变量的好处:
//便于以后的修改:若以后更换变量名,只修改类就可以了,而不用修改对象
//使用更加安全:创建对象时不必知道类的内部成员的名称
{
strcpy(m_brand, brand);
}
void setPrice(float fprice)
{
_fprice = fprice;
}
};
2、类外实现类函数:
实例代码如下(代码3-1):
///
/// @file Computer.cc
/// @date 2019-02-06 14:23:56
///
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
//类方法的外部实现
class Computer
{
public://public的方法视为接口,表示能够对外提供的服务
void print();
void setBrand(const char * brand);
void setPrice(float fprice);
private:
char * m_brand;
float _fprice;
};
void Computer::print()
{
cout << "品牌: " << m_brand << endl;
cout << "价格: " << _fprice << endl;
}
void Computer::setBrand(const char * brand)
{
strcpy(m_brand, brand);
}
void Computer::setPrice(float fprice)
{
_fprice = fprice;
}
在类定义的外部定义成员函数时,应使用作用域操作符(::)来标识函数所属的类,即有如下形式:
返回类型 类名::成员函数名(参数列表)
{
函数体
}
其中,返回类型、成员函数名和参数列表必须与类定义时的函数原型一致。
类的实例化
定义了一个类之后,便可以如同用int、double等类型符声明简单变量一样,创建该类的对象,称为类的实例化。由此看来,类的定义实际上是定义了一种类型,类不接收或存储具体的值,只作为生成具体对象的“蓝图”,只有将类实例化,创建对象(声明类的变量)后,系统才为对象分配存储空间。
示例代码如下(代码3-2):
///
/// @file Point.cc
/// @date 2019-02-06 15:15:01
///
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
void print()
{
cout << "(" << _ix
<< "," << _iy
<< ")" << endl;
}
void setPoint(int ix, int iy)
{
_ix = ix;
_iy = iy;
}
private:
int _ix;
int _iy;
};
int main()
{
Point pt1; //定义一个Point对象
pt1.setPoint(1,2);//调用pt1对象的setPoint方法为_ix,_iy赋值
pt1.print();//调用pt1对象的print方法
return 0;
}
class的定义与使用看上去很像struct定义和使用的扩展,事实上,类定义时的关键字class完全可以替换成struct,也就是说,结构体变量也可以有成员函数。class和struct的唯一区别在于:struct的默认访问方式是public,而class为private。
提示:通常使用class来定义类,而把struct用于只表示数据对象、没有成员函数的类。
对象的作用域、可见域和生存期与普通变量,如int型变量的作用域、可见域和生存期并无不同,对象同样有局部、全局和类内(稍后就将对对象成员进行介绍)之分,对于在代码块中声明的局部对象,在代码块执行结束退出时,对象会被自动撤销,对应的内存会自动释放(当然,如果对象的成员函数中使用了new或malloc申请了动态内存,却没有使用delete或free命令释放,对象撤销时,这部分动态内存不会自动释放,造成内存泄露)。跟踪调试,查看同一个类的不同对象的成员变量和成员函数在内存中的地址分配情况。结论:同一个类的不同对象的成员变量占据不同的内存区域(堆、栈);成员函数共用同一内存区域(代码段)。
类的构造函数与初始化表达式
构造函数
构造函数,在对象创建时自动调用,用以完成对象成员变量等的初始化及其他操作(如为指针成员动态申请内存空间等);如果程序员没有显式的定义它,系统会提供一个默认的构造函数,并在每一次创建对象时自动执行该构造函数。构造函数有一些独特的地方:函数的名字与类名相同,没有返回类型和返回值,即使是void也不能有(void并不是“没有返回值”而是“返回值为空”)。其主要工作有:
- 给对象一个标识符;
- 为对象数据成员开辟内存空间;
- 完成对象数据成员的初始化(函数体内的工作,由程序员完成);
上述3点也说明了构造函数的执行顺序,在执行函数体之前,构造函数已经为对象的数据成员开辟了内存空间,这时,在函数体内对数据成员的初始化便是顺理成章了。实例代码如下(代码3-3):
///
/// @file Point.cc
/// @author XuHuanhuan(1982299154@qq.com)
/// @date 2019-02-06 15:15:01
///
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
//构造函数是创建对象时
//自动运行的不需要单独调用
//,没有构造函数
//编译器也会给对象一个初始值的
Point()//默认(无参)构造函数
{
_ix = 0;
_iy = 0;
cout << "Point()" << endl;
}
//在类中只要显示定义了一个有参构造函数,
//系统就不会再提供默认构造函数,所以,要创建
//一个无参对象就会报错的,避免该错的办法就是再定义一个
//无参构造函数。这说明了构造函数是能够进行重载的
Point(int ix = 0, int iy = 0)
{
_ix = ix;
_iy = iy;
}
void print()
{
cout << "(" << _ix
<< "," << _iy
<< ")" << endl;
}
private:
int _ix;
int _iy;
};
int main()
{
Point pt1(1,2);//有了构造函数就可以在定义时直接初始化该对象了。
pt1.print();
return 0;
}
在类中只要显示定义了一个有参构造函数,系统就不会再提供默认构造函数,所以,要创建一个无参对象就会报错的,避免该错的办法就是再定义一个无参构造函数。这说明了构造函数是能够进行重载的。
初始化表达式
除了在构造函数体内初始化数据成员外,还可以通过成员初始化表达式来完成。成员初始化表达式可用于初始化类的任意数据成员(static数据成员除外),该表达式由逗号分隔的数据成员表组成,初值放在一对圆括号中。只要将成员初始化表达式放在构造函数的头和体之间,并用冒号将其与构造函数的头分隔开,便可实现数据成员表中元素的初始化。示例如下(代码3-4):
///
/// @file X.cc
/// @date 2019-02-06 15:39:59
///
#include <iostream>
using std::cout;
using std::endl;
class X{
public:
X(int a)
:iy(a) //初始化表达式
, ix(iy) //初始化表达式
//初始化表达式的执行顺序是由其初始化变量在
//声明时的顺序。在这里,先定义的ix,后定义的iy
//所以,先初始化ix = iy,在初始化iy = a
//故iy = 3,而ix不等于3
{
cout << "X(int)" << endl;
}
void print()
{
cout << "ix = " << ix
<< "iy = " << iy
<< endl;
}
private:
int ix, iy;
};
int main()
{
X x(3);
x.print();
}
每个成员在初始化表达式中只能出现一次,初始化的顺序不是由成员变量在初始化表中的顺序决定的,而是由成员变量在类中被申明时的顺序决定的,所以为了避免出错,初始化表达式要与变量申明时的顺序一致。理解这一点有助于避免意想不到的错误。另外还要注意的是:初始化成员列表的赋值语句先执行,构造函数体中的赋值语句后执行。
类的析构函数
析构函数在对象被撤销时被自动调用,相比构造函数,析构函数要简单的多。析构函数有如下特点:
- 与类同名,之前冠以波浪号,以区别于构造函数;
- 析构函数没有返回类型,也不能指定参数,因此,析构函数只能有一个,不能被重载;
- 对象超出其作用域被销毁时,析构函数会被自动调用;
如果用户没有显式地定义析构函数,编译器将为类生成“缺省析构函数”,缺省析构函数是个空的函数体,只清除类的数据成员所占据的空间,但对类的成员变量通过new和malloc动态申请的内存无能为力,因此,对于动态申请的内存,应在类中构造析构函数,通过delete或free进行释放,这样能有效避免对象撤销造成的内存泄漏。
实例代码如下(代码3-5):
///
/// @file Computer.cc
/// @author XuHuanhuan(1982299154@qq.com)
/// @date 2019-02-06 14:23:56
///
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Computer
//构造与析构函数都是特殊的函数,他们都没有返回值,
//在类内部实现的函数都是inline函数,即在预处理时就会用
//相应的代码替换到被调用的位置去。
{
public:
Computer(const char * brand, float fprice);
void print();
~Computer();
private:
char * m_brand;
float _fprice;
};
Computer::Computer(const char * brand, float fprice)
:m_brand(new char[strlen(brand) + 1])//对m_brand的操作用到了
//深拷贝:使用了堆空间存放变量
,_fprice(fprice)
{
cout << "Computer(const char * , float)" << endl;
strcpy(m_brand, brand);
}
void Computer::print()
{
cout << "品牌: " << m_brand << endl;
cout << "价格: " << _fprice << endl;
}
//由于使用了深拷贝,所以需要使用析构函数来回收分配的空间
//对于栈对象而言,当其生命周期结束时,就会自动执行析构函数
//析构函数可以显示调用,但是不推荐这样做
//~~~~~~~~析构函数~~~~~~~~~~
Computer::~Computer()
{
delete [] m_brand;
cout << "~Computer" << endl;
}
//~~~~~~~~~~~~~~~~~~~~~~~~
Computer pc3("HP", 3000);//全局对象也会自动执行析构函数
int main(void)
{
Computer * pc2 = new Computer("Mac", 10000);//pc2是一个指向
//堆对象的指针,该堆对象是不会自动执行析构函数的,所以,需要手动将该
//对象删除。这时程序才会自动执行析构函数。所以,堆对象必须要手动删除
Computer c1("Lenovo", 5000);
c1.print();
pc2->print();
delete pc2;
pc3.print();
return 0;
}
构造函数与析构函数的执行时间对比
- 对于全局定义的对象,每当程序开始运行,在主函数main接受程序控制权之前,就调用全局对象的构造函数。整个程序结束时调用析构函数。
- 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
- 对于关键字static定义的静态局部变量,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
- 对于用new运算符创建的对象,每当创建该对象时调用构造函数,当用delete删除该对象时,调用析构函数。
类的复制构造函数
以上面的Point类为例,当执行point pt1(2,3); point pt2=pt1(或者point pt2( pt1););
语句时,上述语句用pt1初始化pt2,相当于将pt1中每个数据成员的值复制到pt2中,这是表面现象。实际上,系统调用了一个复制构造函数,来实现该对象赋值功能的。如果类定义中没有显式定义该复制构造函数时,编译器会隐式定义一个缺省的复制构造函数,它是一个inline、public的成员函数,其原型形式为:类名::类名(const 类名 &)
如:point:: point (const point &);
。示例如下(代码3-6):
///
/// @file Copy.cc
/// @date 2019-02-06 19:43:21
///
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Point
{
public:
Point(int ix, int iy)
:_ix(ix)
,_iy(iy)
{
cout << "Point()" << endl;
}
void print()
{
cout << "_ix = " << _ix << endl;
cout << "_iy = " << _iy << endl;
}
~Point()
{
cout << "~Popint()" << endl;
}
//复制构造函数,该函数在系统中默认生成。
Point(const Point & rhs)
:_ix(rhs._ix)
,_iy(rhs._iy)
{
cout << "Point(Popint rhs)" << endl;
}
private:
int _ix, _iy;
};
class Brand
{
public:
Brand(const char * brand, float fprice)
:_brand(new char[strlen(brand)+1])
,_fprice(fprice)
{
strcpy(_brand, brand);
cout << "Brand(const char * brand, float fprice)" << endl;
}
void print()
{
cout << "_brand: " << _brand << endl;
cout << "_fprice " << _fprice << endl;
}
~Brand()
{
delete [] _brand;
cout << "~Brand()" << endl;
}
//显示的构造复制构造函数
//~~~~~~~~~~~~~~~~~~~~~~~~
Brand(const Brand & rhs)
:_brand(new char[strlen(rhs._brand)+1])
,_fprice(rhs._fprice)
{
strcpy(_brand, rhs._brand);
cout << "Brand(const Brand & rhs)" << endl;
}
//~~~~~~~~~~~~~~~~~~~
private:
char * _brand;
float _fprice;
};
//当函数的参数为对象时,实参与形参会运行复制构造函数
void func1(Point p)
{
p.print();
cout << "func1 " << endl;
}
//函数的返回值是对象是,也会运行复制构造函数(编译器隐形调用,并不会
//在运行时显示出来,需要在编译时添加参数才能看出来:
//g++ Copy.cc -fno-elide-constructors)
Point func2()
{
Point p(1,2);
p.print();
cout << "in func2`````" << endl;
return p;
}
int main()
{
Point pt1(1,2);
cout << "func1-----" << endl;
func1(pt1);
cout << "func1-----" << endl;
Point pt4 = func2();
cout << "func2----------" << endl;
pt4.print();
cout << "func2--------" << endl;
Point px1(Point(1,2));//如果复制构造函数去掉const,那么
//Point(1,2)就没有办法被传到形参上去。原因在于,Point(1,2)存在
//的时间太短,短到创建完立即被释放。
px1.print();
Point pt2 = pt1;
cout << "pt1 = " << endl;
pt1.print();
cout << "pt2 = " << endl;
pt2.print();
Brand br("Lenovo", 5000);
br.print();
Brand br1 = br;//使用编译器默认的复制构造函数,编译可以通过
//但是运行时出现core错误。原因在于系统提供的复制构造函数复制的
//只是指针而没有复制内容。但两个对象被销毁时两个指针指向了同一个地址
//故该地址被销毁的两次。所以造成错误
br1.print();
return 0;
}
复制构造函数相比于构造函数和析构函数都要难。体现在三点:
- 执行的情况更为多样;
- 实现的细节相比较多;
- 更难理解;
执行的情况更为多样
复制构造函数被执行的情况有:
- 对象的赋值语句,如:
Point pt1(1,2);Point pt2 = pt1; Point pt3(Pt1);
后两句都会执行复制构造函数 - 当实参和形参都是对象,进行形参和实参的结合时,如:
//Point是一个类
void func(Point x)
{
cout << "func1()" << endl;
return 0;
}
int main()
{
Point pt1(1,2);
func(pt1); //该句执行复制构造函数
return 0;
}
- 当函数的返回值是对象,函数调用完成返回时,该情况不像上面两个看出来。这是因为编译器做了相关的优化,可以通过编译指令:
g++ -fno-elide-constructors C++file.cc
生成a.out文件,然后运行就可以看到其调用了复制构造函数如:
//Point是一个类
Point func(int x)
{
Point pt(x,2);
cout << "func1()" << endl;
return pt;
}
int main()
{
int x = 1;
Point pt = func(x); //该句执行复制构造函数
return 0;
}
实现的细节相比较多
缺省的复制构造函数并非万金油,在一些情况下,必须由程序员显式定义缺省复制构造函数.就如“代码3-6”的Brand类所示,由于其使用了深拷贝,所以经过这样赋值后,两个对象的指针都指向了同一块内存(因为系统提供的复制构造函数只是将指针所指向的地址给复制了一份,而没有复制其地址所指向的内容),当两个对象释放时,其析构函数都要delete[]同一内存块,便造成了2次delete[],从而引发了错误。所以需要显示的定义构造函数。实例代码如“代码3-6”的Brand类中所示。
更难理解
为什么复制构造函数的定义是这样的类名::类名(const 类名 &)
,具体的说有两个问题:
- 为什么形参会有const关键字:
//Point 为类
int main()
{
Point pt(Point(1,2));
}
如果复制构造函数去掉const,那么上述代码的Point(1,2)就没有办法被传到形参上去。原因在于,Point(1,2)存在的时间太短,短到创建完立即被销毁,连地址都没取到就销毁了。所以问题又来了:==const关键字到底是什么功能?,==目前还没有一个让我满意的解释,不过这篇博文可以看看。
- 为什么形参会有引用&:函数参数为对象时,在传递值时就会调用复制构造函数。而复制构造函数本身也是形参为对象的函数,那么当它被运行时就会调用他自己,于是造成了,无穷的递归循环之中。解决的办法就是在处理函数见传值时避免复制,而使用引用(&)就可以实现不用复制对象而是直接使用原来的对象进行操作。