文章目录
构造函数和析构函数(有特殊专门使命的公有成员函数)
这两个标准函数,用于创建和删除当前类的对象
构造函数(constructor,名称和类名相同,使得可以像初始化内置类型那样初始化类对象)
一般我们喜欢在声明变量的同时就赋值,即初始化。对象也是如此,我们也希望可以直接初始化。毕竟C++的目标之一是要让类和内置类型尽量相似,所以C++当然要努力实现相似的初始化这件事。
以前初始化普通内置类型
int cat = 10;
struct thing
{
char * pc;
int m;
}
thing a = {"apple", 5};
但是类对象的数据成员一般是私有的,程序不可以直接访问,只能用公有成员函数来访问,所以初始化类对象必须要设计一个专用的公有的成员函数——类构造函数。
它专门用于构造新对象,以及给新对象的数据成员们赋值。
我们上篇文章的例程没有初始化类对象:
#include <iostream>
#include "stock.h"
int main()
{
//用声明类对象的方式创建一个对象,但是没有初始化!
//这时候fluffy对象的成员们都是没有值的
//如果在调用acquire之前就调用show等成员函数,就会出错
Stock fluffy;
fluffy.acquire("NanoSmart", 20, 12.50);//用成员运算符调用类方法
fluffy.show();
fluffy.buy(15, 18.125);
fluffy.show();
fluffy.sell(400, 20.0);
fluffy.show();
fluffy.buy(300000, 0.125);
fluffy.show();
return 0;
}
构造函数没有返回值,也没有类型(返回值不是void类型!)
//stock.h
#ifndef STOCK_H_
#define STOCK_H_
#include <string>
class Stock //类声明 class declaration
//一个常见但不通用的约定:类名首字母大写
{
private://可不写,类对象的默认访问控制就是private
std::string company;
long shares;
double share_val;
double total_val;
//可以在成员函数中使用数据成员
//定义位于类声明中的函数自动成为内联函数,无需关键字inline
void set_tot(){total_val = shares * share_val;}
public:
Stock(const std::string & co, long n, double pr);//构造函数
void acquire(const std::string & co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};//像结构体声明一样,有分号
#endif // STOCK_H_
//Stock.cpp
#include <iostream>
#include "stock.h"
//构造函数没有返回值,参数的数目和类型根据类的私有成员确定
Stock::Stock(const std::string & co, long n, double pr)
{
//千万不要把构造函数的形参名称设置为和类数据成员一样
//否则会出现company = company;的错误语句
company = co;
if (n < 0)
{
std::cerr << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
声明对象时,将自动调用构造函数
不能使用对象来调用构造函数,因为使用构造函数之前,对象都还没被创建出来呢,根本不存在。
//显式调用构造函数
Stock food = Stock("World Cabbage", 240, 12.25);
//隐式调用构造函数,更紧凑
Stock garment("Furry Mason", 30, 4.45);
//用new动态分配内存,创建匿名对象,使用对象指针管理此对象
Stock *pstock = new Stock("Electroshock Games", 18, 19.0);
但是如果我提供了构造函数,却还像之前那样不初始化,即不提供初始化的值,则出错,这是因为编译器不提供默认构造函数了,下面说
默认构造函数(没返回值还没参数,只创建对象,但不初始化成员)
其实Stock food;这句代码在不提供构造函数时没有报错,是因为C++提供了默认构造函数,它只创建对象,而不给对象的数据成员赋值,其代码类似于
Stock::Stock(){}
可以看到,默认构造函数不仅没有返回值,还没有参数,因为不初始化呀。
当且仅当你自己没提供构造函数时,编译器才会提供默认构造函数
所以只要你定义了构造函数,你就必须再通过重载定义一个默认构造函数,才能使得Stock food;这样的代码不出错
自己定义默认构造函数的方法1:没有参数
Stock();
设计类时,提供一个给所有成员提供隐式初始化值的默认构造函数
自己定义默认构造函数的方法2:给所有数据成员提供隐式初始化值
但是通常我们希望创建对象时还是初始化一下成员,所以不建议上面那种类似于编译器给的默认构造函数一样,什么参数也没有,什么也不做。而是给所有成员提供一个初始化值:
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
示例
头文件中
public:
Stock(const std::string & co, long n, double pr);//构造函数
Stock();//默认构造函数
//Stock.cpp
#include <iostream>
#include "stock.h"
//构造函数没有返回值,参数的数目和类型根据类的私有成员确定
Stock::Stock(const std::string & co, long n, double pr)
{
//千万不要把构造函数的形参名称设置为和类数据成员一样
//否则会出现company = company;的错误语句
company = co;
if (n < 0)
{
std::cerr << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
//默认构造函数
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
这样这句代码就不会报错了
只接受1个参数的构造函数允许使用赋值语法把对象初始化为一个值,但这种特性易导致问题,可关闭该特性
因为这样实际上是一种隐式类型转换,把其他类型转换为类类型。
虽然看起来很方便,但是却可能带来令人沮丧的意外
为什么构造函数不能被继承
涉及继承的话,派生类的默认构造函数会做什么
后面会讲继承,先提前说一下,派生类的默认构造函数即使函数体为空,且没有用初始化成员列表显式调用基类构造函数,那派生类的构造函数也会自动调用基类的默认构造函数以初始化基类部分的数据成员。
如果派生类没写构造函数,那么编译器就会给他使用默认构造函数。
且派生类的默认构造函数会自动调用基类的默认构造函数,因为没有代码,就不能传参,想调用不默认的也不行啊。
需要注意的是,如果基类中自己定义了构造函数,编译器就不会给默认构造函数了,这时候派生类的调用就会出错。所以只要你定义自己的构造函数,就必须同时定义自己的默认构造函数,常用的一个办法是给自己的构造函数的所有参数设置默认值,这样他就既可以充当默认构造函数,阻止刚才那种情况下的报错,还可以自己输入参数初始化对象,可以保证任何情况下对象都可以被正确初始化。(一个函数做两件事,这就是默认参数的牛逼之处)
析构函数(destructor,也被自动调用,完成清理工作,没有参数,函数名为类名前加个波浪线,其工作非常轻松稀少)
析构函数比构造函数简单一些,因为他的工作很少,有时候甚至没有工作
构造函数创建对象后,程序就会一直跟踪这个对象,直到这个对象被销毁,这时候会自动调用析构函数,做一点特别轻松的清理工作。
比如如果构造函数用了new分配内存,那析构函数就delete一下,除此之外就没什么事情做了。确实是个闲职。
析构函数像默认构造函数那样,没有参数。且他一般也没有返回值。
所以不做什么的时候,析构函数长这样:
Stock::~Stock()
{
}
但是一旦构造函数使用了new,则析构函数必须使用delete
编译器啥时候自动调用析构函数(对象过期时)
一般析构函数都是编译器自动调用的,我们不要显式在程序中调用它。
什么叫过期,就是存储期过了
比如如果是静态存储类别的对象,那就在程序结束时指定调用析构函数;
如果是自动存储类别的对象,那就在对象被定义的块执行结束时调用
如果是new创建的,那这个对象就会一直驻留在堆中,直到delete释放它为止。delete来释放他的时候,编译器会自动调用析构函数。
默认析构函数(只有在程序员不提供时编译器才提供)
和默认构造函数一样,只有在程序员不提供时编译器才提供
具体提供的操作:
编译时,编译器看到类声明中没有提供析构函数,则立刻隐式声明一个默认析构函数;
然后在执行代码时,如果看到要删除对象或者导致对象被删除的代码(比如定义对象的块执行完毕···),编译器就提供默认析构函数的定义。
基类必须有虚析构函数
示例 给已初始化的类对象赋新值,会创建临时对象
给已初始化的类对象赋新值时,可以通过初始化,也可以赋值,但是书上说通过初始化的效率更高,应该尽量使用初始化的方式,虽然不知为何,但是先记住。
毕竟赋值只是赋值所有数据成员,而初始化方式又要调用构造函数,又要创建临时变量,还要把临时变量的所有数据成员复制,感觉应该是更慢呀
//stock.h
#ifndef STOCK_H_
#define STOCK_H_
#include <string>
class Stock //类声明 class declaration
//一个常见但不通用的约定:类名首字母大写
{
private://可不写,类对象的默认访问控制就是private
std::string company;
long shares;
double share_val;
double total_val;
//可以在成员函数中使用数据成员
//定义位于类声明中的函数自动成为内联函数,无需关键字inline
void set_tot(){total_val = shares * share_val;}
public:
Stock(const std::string & co, long n, double pr);//构造函数
Stock();//默认构造函数
~Stock();//析构函数destructor
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};//像结构体声明一样,有分号
#endif // STOCK_H_
//Stock.cpp
#include <iostream>
#include "stock.h"
//构造函数没有返回值,参数的数目和类型根据类的私有成员确定
Stock::Stock(const std::string & co, long n, double pr)
{
//千万不要把构造函数的形参名称设置为和类数据成员一样
//否则会出现company = company;的错误语句
std::cout << "Constructor using " << co << " called.\n";
company = co;
if (n < 0)
{
std::cerr << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
//默认构造函数
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
std::cout << "Default constructor called.\n";
}
//析构函数
Stock::~Stock()
{
std::cout << "Bye! " << company << "\n";
}
//类的成员函数的定义都必须用作用域解析运算符标识函数所属的类
void Stock::buy(long num, double price)
{
//增加持有的股份
if (num < 0)
{
std::cout << "Number of shares purchased can't be nagative. "
<< "Transaction is aborted.\n";
}
else
{
shares += num;
share_val = price;
set_tot();
}
}
void Stock::sell(long num, double price)
{
//减少持有的股份
if (num <0)
{
std::cout << "Number of shares sold can't be negative. "
<< "Transaction is aborted.\n";
}
else if (num > shares)
{
std::cout << "You can't sell more than you have! "
<< "Transaction is aborted!\n";
}
else
{
shares -= num;
share_val = price;
set_tot();
}
}
void Stock::update(double price)
{
share_val = price;
set_tot();
}
void Stock::show()
{
using std::cout;
using std::ios_base;
//set format to #.###
ios_base::fmtflags orig =
cout.setf(ios_base::fixed, ios_base::floatfield);
std::streamsize prec = cout.precision(3);
cout << "Company: " << company
<< " Shares: " << shares << '\n'
<< " Share Price: $" << share_val;
//set format to #.##
cout.precision(2);
cout << " Total Worth: $" << total_val << '\n';
//restore original format
cout.setf(orig, ios_base::floatfield);
cout.precision(prec);
}
#include <iostream>
#include "stock.h"
int main()
{
//为了看清析构函数被调用,所以在return之前弄个块
{
using std::cout;
cout << "Using constructors to create new objects\n";
Stock stock1("NanoSmart", 12, 20.0);//非默认构造函数
stock1.show();
Stock stock2 = Stock("Boffo Objects", 2, 2.0);//非默认构造函数
stock2.show();
cout << '\n';
cout << "Assigning stock1 to stock2:\n";//同类型对象之间赋值,把源对象的每个数据成员的值赋给目标对象
stock2 = stock1;//赋值并不会调用构造函数,因为不会创建对象
cout << "Listing stock1 and stock2:\n";
stock1.show();
stock2.show();
cout << '\n';
cout << "Using a constructor to reset an object\n";
//使用构造函数重新给对象赋值时,实际上是重新创建了一个临时对象,把临时对象赋值给stock1
//然后把临时对象析构
//由于stock1对象已经存在,所以这句代码不是初始化,而是赋新值
stock1 = Stock("Nicky Foods", 10, 50.0);//临时对象,非默认构造函数,立刻调用析构函数
cout << "Revised stock1:\n";
stock1.show();
cout << "Done.\n";
}//自动调用析构函数,清理stock1和stock2
return 0;
}
输出
Using constructors to create new objects
Constructor using NanoSmart called.
Company: NanoSmart Shares: 12
Share Price: $20.000 Total Worth: $240.00
Constructor using Boffo Objects called.
Company: Boffo Objects Shares: 2
Share Price: $2.000 Total Worth: $4.00
Assigning stock1 to stock2:
Listing stock1 and stock2:
Company: NanoSmart Shares: 12
Share Price: $20.000 Total Worth: $240.00
Company: NanoSmart Shares: 12
Share Price: $20.000 Total Worth: $240.00
Using a constructor to reset an object
Constructor using Nicky Foods called.
Bye! Nicky Foods
Revised stock1:
Company: Nicky Foods Shares: 10
Share Price: $50.000 Total Worth: $500.00
Done.
Bye! NanoSmart
Bye! Nicky Foods
示例 用C++11新增的列表初始化初始化类对象(调用析构函数时的顺序和调用构造函数的顺序相反,因为使用的是栈内存)
把上面的主程序改为
#include <iostream>
#include "stock.h"
int main()
{
//为了看清析构函数被调用,所以在return之前弄个块
{
using std::cout;
//使用列表初始化方式初始化类对象
Stock stock1 = {"NanoSmart", 12, 20.0};//非默认构造函数
Stock stock2 {"Boffo Objects", 2, 2.0};//非默认构造函数
Stock stock3 {};//默认构造函数
stock1.show();
stock2.show();
stock3.show();
}//自动调用析构函数,清理stock1和stock2
return 0;
}
输出
可以看到,默认构造函数给数据成员们赋了我自己设置的初始化值。
并且,还可以看到,调用析构函数时的顺序和调用构造函数的顺序相反!!!这是因为这三个对象都没有使用new,所以都是自动存储类别,使用的是栈内存,自然就是LIFO。
Constructor using NanoSmart called.
Constructor using Boffo Objects called.
Default constructor called.
Company: NanoSmart Shares: 12
Share Price: $20.000 Total Worth: $240.00
Company: Boffo Objects Shares: 2
Share Price: $2.000 Total Worth: $4.00
Company: no name Shares: 0
Share Price: $0.000 Total Worth: $0.00
Bye! no name
Bye! Boffo Objects
Bye! NanoSmart
const成员函数(只要成员函数不修改调用对象,就将其声明为const成员函数)
就像之前说把指针和引用作为函数形参时应尽量多用const声明一样,只要方法不改调用对象,就把他声明为const的
但是之前是说const参数,现在说的是const函数,怎么写呀
答案是,凑合找个地方呗
不能写在返回值前面,因为那限定的是返回值。
也不能写在函数名前面,因为在返回类型有指针时,这样做表示const指针
int * const ft(int);
更不能写在参数前面,因为那限定的是参数。
为了限定整个函数,只能是放在方法原型的分号前面了,即函数头的末尾,因为只有这个地方没有被被人占,没有二义
所以const成员函数的声明和定义:
//声明
void show() const;
//定义
void show() const
{
}
const类对象必须使用const成员函数
如图,把stock1对象限定为const,就无法调用show方法了,因为show方法不是const成员函数,编译器觉得有修改stock1对象的风险
把show方法的原型和定义屁股上都加个const就好了