面向对象编程的整体工作分为2大块,一个是建模(将任务抽象成类)和编写类库(类的方法实体编写),另一个是使用类库来编写主程序完成业务。有些人只负责建模和编写类库,这些都是高手,还有些人是调用现成的类库来编写自己的主任务程序。
c++的四重境界
- 第一重:语法层面,对语法比较熟悉,会使用c++的语法来建模、编程。
- 第二重:基于第一重,能使用c++来解决实际问题。我们工作大部分时候在这一重。
- 第三重:编写库给别人用,出了问题能快速解决,这种人基础比较强,且有一定的框架思维。
- 第四重:理解c++的语法设计的背后的原因。有自己的独立思考能力,能思考如果让他来设计,也会像c++作者这样来设计,这种已经把c++上升到哲学的境界了,非常恐怖。
构造与析构函数
构造函数constructor,是用来初始化对象的函数,当我们在栈上定义一个对象或在堆中new一个对象的时候就会自动调用构造函数。构造函数一般用于初始化class的属性、分配class内部需要的动态内存。
析构函数destructor,是用来销毁对象的工作痕迹,在程序结束时,被自动调用,析构函数一般用来回收构造函数中申请的动态内存,避免造成内存丢失。
构造和析构函数是c++面向对象编程的一大语言特性,如果我们没有提供构造函数和析构函数,会使用c++默认的构造与析构函数。当然我们也可以显示提供构造函数和析构函数。
构造与析构函数的应用
默认的构造函数不需要返回值,可以带参也可以不带参,析构函数是不带参数的,构造的默认函数名和类名相同,而析构函数的函数名是类名前面多一个“~”取反符号,示例如下:
class person //类名
person( ) //默认构造函数,无需写返回值void和形参void
~person( ) //默认的析构函数,无需写返回值void和形参void
按上示例,将构造和析构函数声明直接写在类中,函数实体可以像其他函数一样写在外面。析构函数不需要重载,而构造函数可以重载(有多个函数名相同的函数,只是形参不同),如下示例:
person(); // person类默认构造函数,显式提供。
person(int a); // person类默认带参构造函数
person *p = new person ; //新建对象,自动匹配无参构造函数person();
person *p = new person(5); //新建对象,自动匹配有参构造函数person(int a);
通常我们在写代码的时候会建立一个专门建模(创建类)的头文件.hpp,再建一个.cpp文件,用来编写类中的构造、析构函数、及类中的方法实体。就像本文开始提到的,有的人是专门建模和编写类库。那么建模者把这些东西独立成文件,是顺利成章的。
构造与析构之动态内存
析构函数的使用
析构函数在对象被销毁时自动被调用。一般有两种情况,一个是我们的对象在栈上,当函数执行完成,系统自动回收栈内存时调用析构函数,另一个是在堆上new了一个对象,程序结束时使用delete释放堆内存时调用析构函数。在一般情况下,析构函数是空的,因为啥也不用做。只有我们在类中申请了动态内存,函数结束时需要释放,那么就可以放在析构函数中去执行。要注意的是:
- 释放单个类型对象可以直接使用delete xxx (xxx代表指针变量)
- 释放一个数组使用delete[ ] xxx (xxx代表指针变量)
实际上我们工作中很少使用上面两种方式来申请动态内存,更多的时候是使用vector,后面再来讨论。
构造函数的细节
构造函数的一大功能就是初始化成员变量,然而在初始时有以下细节要注意。
- 默认的构造函数是无参的,所以无法初始化。
- 一个对象,若无自定义的带参构造函数,那么默认的构造函数可以省略,c++会为我们免费提供。
- 如果我们写了带参函数,那么c++将不会再为我们提供不带参的构造函数,此时如果去调用默认的不带参的构造函数,编译器将报错。
- 当我们把对象定义栈上,定义对象时,如果不带参数,后面不能有括号( ),而要带参初始化则需要括号。而如果new在堆上,则都可以有括号,这一点确实恶心,如下示例:
class person dai; //定义一个类(简写)
person dai(); //错误语法,栈上无参,不能有括号
person *hdz = new person(); //new时可以带括号
person *hdz = new person; //new也可以不带括号
valgrind工具查看内存泄漏
之所以c++难,其实主要是我们要对内存操心,当我们程序太大后内存申请后容易忘记释放,导致程序吃内存,所以检测程序是否有内存泄漏比较重要,在此介绍一个工具来做内存检测valgrind。
valgrind工具介绍
Memcheck是valgrind应用最广泛的内存检查器,能够发现开发中绝大多数内存错误使用情况,除了内存检查还包了以下功能:
- Callgrind—用于检查程序中函数调用过程中出现的问题。
- Cachegrind—用于检查程序中缓存使用出现的问题。
- Helgrind—用于检查多线程程序中出现的竞争问题。
- Massif—用于检查程序中堆栈使用中出现的问题。
安装valgrind
命令:sudo apt-get install valgrind
memcheck使用
- 编译:重新编译要检查的源码,编译时添加-g生成dbug版本目标文件。
g++ person.cpp main.cpp -g -o apptest
- 检查内存:实际上该工具就是对代码运行过程的内存申请和释放统计。./后面跟被检查的可执行文件,
valgrind --tool=memcheck --leak-check=full --show-reachable=yes --trace-children=yes ./app
成员初始化列表
当我们要给对象内的变量赋初值时可以在构造函数中处理,如下:
person::person(string n,int a,bool s)
{
this->name = name;
this->age = a;
this->sex = s;
}
person p1(“hu”,32,1);//定义一个person类型的变量p1,并传参给构造函数。
这种方式也可以,但c++为我们提供了更简单的方法“成员初始化列表”,示例如下:
person::person(string n,int a,bool s):name(n),age(a),sex(s) //实体函数带参数列表
上例中,在函数后面添加“:”来表达后面的是参数列表,多个参数用“,”隔开,而定义在类中的函数声明不能有参数列表。
构造函数默认值
在class定义中函数声明可以给默认值,当没有传参时编译器就会使用默认值,但是函数实体不能有默认值,示例如下:
class person
{
public:
person(string n="lilei",int a=6,bool s=1); //构造函数声明时给默认值
};
有部分默认值时,默认值不能写在前面,必须写后面,否则默认值没有意义,如示例一,因为构造函数person形参a没有默认值,所以在调用时a必须传参,而a在后面,要想给a传参,必须传2个参数,前一个默认值则被覆盖,这样默认值就失去了意义。正确的写法如示例二,在调用时传1个参数就能对a赋值,而n使用默认值,传2个参数,都覆盖。
示例一:person(string n="lilei",int a); //错误
示例二:person(int a , string n="lilei" ); //正确
调用歧义
函数声明带默认值时,容易出现调用歧义,需要注意,如示例一, person的声明1片顶3片,如示例二,无论是我们怎么调用,都可以重载调用到示例一。所以当我们同时出现示例二种的任意2个调用,都会相互冲突,编译不知道该与谁匹配,就会报错。所以很多时候为了避免歧义,干脆都不带默认值。
示例一:person(string n="lilei",int a=6,bool s=1);//默认全带参构造函数
示例二:person( );
person(“han” );
person(“han”,10 );
person(“han”,10,flas ),