类构造函数:
构造函数 是专门用于构造新对象、将值赋给它们的数据成员。
C++为这些成员提供了名称和使用语法,而程序员需要提供方法定义。名称与类名相同。
例如:Stock类的一个可能的构造函数是名为Stock()的成员函数。
构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型。
声明和定义构造函数:
和使用普通函数(根据传递的参数给私有成员赋值)几乎一样,除了构造函数的函数名需要和类名保持一致。
例如:
void Player::Player(std::string name, double hps, double atks)
{
Name = name;
Hp_s = hps;
Atk_s = atks;
hp_();
atk_();
}
这里将(一一五)的一个源代码文件中,Player类的start函数名改为了Player(和类名保持一致),这样的话,就可以在声明类对象的时候,同时对其初始化了。。
需要注意的是,传递的参数名(std::string name, double hps, double atks)不能是私有成员的名字。否则调用本函数,实际上便是将私有成员的值赋给自己。
如代码:
#include<iostream> #include<string> class man //创建类 { private: std::string name; //私有成员两个 int year; public: man(const char*na, int a) //构造函数名同类名 { name = na; //传递参数来赋值 year = a; } void show() //显示 { using namespace std; cout << name << " is " << year << " years old." << endl; } }; int main() { using namespace std; man a = { "wd",27 }; a.show(); system("pause"); return 0; }
输出:
wd is 27 years old. 请按任意键继续. . .
构造函数的调用方式:
①显式的:如man a = man("aa", 1); //注意,这里是小括号(圆的)
②隐式的:如man b("bb", 1); //注意,这里也是小括号(圆的)
③C++11的列表初始化:如
man c = man{ "cc",1 };
man d = { "dd",1 };
man f{ "ff",1 };
以上三个是大括号,区别与小括号。小括号不支持第二种方式
④使用new时:如
man *a = new man{ "wd",27 }; //初始化
(*a).show(); //调用需要加括号,不能是*a.show()这样
⑤使用构造函数时,必须在声明的时候进行初始化,否则会提示类不存在默认构造函数。例如上面代码中,直接写man a; 编译器是会提示错误的;
⑥注意:无法使用对象名来调用构造函数(会提示使用的类型名);
一般来说,构造函数是创建对象时使用的。
⑦构造函数的参数,可以设置默认参数,就像使用带默认参数的函数那样使用(遵守相关规定)。如:man(const char*na = "xxx", int a = 10)
默认构造函数:
所谓默认构造函数,指的是在未提供显式初始值时,用来创建对象的构造函数。
例如,一个类,只有私有成员和公有成员(数据和函数),但未提供默认构造函数。那么C++将自动提供默认构造函数的隐式版本,不做任何工作(即可以只声明对象,但是不赋值)。
例如,若上面的man类未提供构造函数的话,那么其默认构造函数可能是:
man(){}
即既无参数,也无函数代码。
如果要使用默认构造函数,那么就应该给对象的私有成员进行赋值(至少,要有函数定义,不能只有函数原型),否则编译器就会提示出错(实际上也容易出问题)。
例如,至少是这样:
man(const char*na = "xxx", int a = 10) {}//使用默认构造函数的最简形式,注意,这里的参数并没有意义
和man() {}这种隐式的默认构造函数,是等价的;
推荐是这样:
man(const char*na = "xxx", int a = 10) //构造函数名同类名
{
name = na; //传递参数来赋值
year = a;
}
或者这样(效果相同,但前者可以多一个选择,以便在初始化时赋值):
man() //默认构造函数
{
name = "xxx"; //提供默认值
year = 1;
}
这样的话,假如用户在初始化的时候,若不赋值,则自动使用默认值。
析构函数:
用构造函数(无论是默认的还是用户自己定义的)创建对象后,程序负责跟踪该对象,直到过期为止。
对象过期时,程序将自动调用一个特殊的成员函数——析构函数。
析构函数负责完成清理工作,按照教程所说,其很有用。
例如,如果构造函数使用new来分配内存,那么析构函数将使用delete来释放这些内存。(但貌似如果没有使用new,那么析构函数就将无事可做)
如果析构函数无事可做,那么就让编译器生成一个什么都不做的隐式析构函数。
构造名称的函数名,和类名是一样的;
而析构函数的名字,和类名也一样,不过前面还要额外加“~”。
例如:~man() {}这样
析构函数的原型:
析构函数没有参数,所以其函数原型必然是: ~类名(); 这样
因为析构函数在上面那段代码里,并没有做什么事,所以使用的是隐式析构函数。但如果要展现,也可以为析构函数编写代码。如:
~man() { std::cout << "end" << std::endl; }
为了方便查看,我们创建一个块内的类对象,然后观察其在块结束时析构函数的作用,如代码:
#include<iostream> #include<string> class man //创建类 { private: std::string name; //私有成员两个 int year; public: man() //默认构造函数 { name = "xxx"; //提供默认值 year = 1; } void ab() { name = "aaa";year = 5; } void show() //显示 { using namespace std; cout << name << " is " << year << " years old." << endl; } ~man() { std::cout << "end" << std::endl; } }; int main() { using namespace std; { //用括号括起来的块,类对象只在块内存在 man a; a.show(); cout << "这里还在块内" << endl; } cout << "这里在块外了" << endl; system("pause"); return 0; }
输出:
xxx is 1 years old. 这里还在块内 end 这里在块外了 请按任意键继续. . .
在离开块后,析构函数执行了,因此多了一行end。
何时调用析构函数:
通常由编译器决定,通常不应在代码中显式的调用析构函数(有关例外情况,需要到12章的“再谈定位new运算符”)。
①如果创建的是静态存储对象,则析构函数将在程序结束时自动调用(因为静态存储对象持续到程序结束)。
②如果创建的是自动存储类对象,那么析构函数将在程序执行完代码块时自动被调用(如上面那段函数)。
③如果对象是通过new创建的,则它将驻留在栈内存或自由存储区之中,当使用delete时,其将被自动调用。
④程序可以创建临时对象来完成特定的操作,在这种情况下,程序在结束对该对象的使用时,自动调用其析构函数(这个没搞明白是什么)。
另外,程序必然有一个析构函数,要么类的编写者提供,要么有程序提供一个隐式析构函数(什么都不干,但需要有)。
另外:析构函数和构造函数的定义,都可以在类外进行定义,只需要在函数内部有一个函数声明即可。
优化类代码,以及类成员函数的各种使用:
如代码:
//1.h 存放类的定义、类成员函数的原型等 #pragma once #include<iostream> #include<string> class man //创建类 { private: std::string name; //私有成员两个 int year; public: man(); //默认构造函数 man(const std::string a, int b = 0); //因为存在重载析构函数,因此不能提供2个默认参数 inline void update() //成员更新,使用内联函数 { name = "帅", year = 99; } void show(); //显示 ~man(); //析构函数,输出文字,表示析构函数确实运行了 }; //1.cpp main()函数,赋值,及调用类方法 #include<iostream> #include<string> #include"1.h" //调用包含类定义的头文件1.h int main() { using namespace std; man b; //使用默认析构函数 b.show(); //调用类方法,输出对象的值 { //用括号括起来的块,类对象只在块内存在 cout << "这里开始在块内" << endl; man a("wd",27); //使用自定义析构函数,并不使用默认参数 a.show(); man b("mmmm"); //使用自定义析构函数,并使用默认参数。另外,这里的b隐藏了块外的b b.show(); b.update(); //更新对象的数据 b.show(); } cout << "这里在块外了" << endl; system("pause"); return 0; } //2.cpp 存放类的函数定义 #include<iostream> #include"1.h" //因为是类的函数定义,因此要加上作用域解析运算符:: man::man() //默认构造函数 { name = "\"NO NAME\""; //提供默认值 year = 0; } man::man(const std::string a, int b) //因为存在重载析构函数,因此不能提供2个默认参数 { name = a; if (b < 0) { std::cout << "人不可能小于0岁,因此,设置为0岁" << std::endl; b = 0; } else year = b; } void man::show() //显示 { using namespace std; cout << name << " is " << year << " years old." << endl; } man::~man() //析构函数,输出文字,表示析构函数确实运行了 { std::cout << name << " end." << std::endl; }
输出:
"NO NAME" is 0 years old. 这里开始在块内 wd is 27 years old. mmmm is 0 years old. 帅 is 99 years old. 帅 end. wd end. 这里在块外了 请按任意键继续. . .
总结:
①块内,类对象a先声明并定义,类对象b其后。由于是自动变量,遵循了LIFO原则,因此后定义的类对象b被首先执行析构函数;
②可以在头文件声明类,然后在源代码文件中定义类的成员函数;
③类的构造函数可以使用重载函数,也可以给重载函数使用默认参数;
但是需要注意的是,如果一个重载函数无参数,那么另一个重载函数就不能同时给所有参数带默认值(会引起重载函数调用不明确,而导致冲突问题);
④不知道为何,似乎没办法在这里使用内联函数,例如: inline void man::update() 会提示说该函数在main()函数中被调用。
⑤使用内联函数的话,那么函数定义应放在类声明里面(即放弃使用函数原型,直接把函数定义放在函数原型原本的位置)。
C++11的列表初始化:
如代码:
//这两个是小括号,构造函数可用 man a = man("aa", 1); //注意,这里是小括号(圆的) man b("bb", 1); //注意,这里也是小括号(圆的) //这三个是大括号,是C++11增加的列表初始化 man c = man{ "cc",1 }; man d = { "dd",1 }; man f{ "ff",1 };
另外,C++11还提供了名为std::initialize_list的类,可将其用做函数参数或方法参数的类型。这个类可以表示任意长度的列表,只要所有列表项的类型都相同或可转换为相同的类型(第16章,所以我完全看不懂)。
const成员函数:
假如一个类在声明的时候,被const所限制,例如:const man a = man("aa", 1);
那么在使用类时,有一些函数将被拒绝使用,原因是编译器不确定你调用的函数是否会修改类对象的数据(比如将某个私有对象成员的值加一)。
由于要确保私有对象成员的值不被修改,因此函数声明和函数定义也应做一定的变化(用const关键字进行限定)。
但类对象的限定方法不同于一般函数,其const关键字应后置于括号之后。
如代码:
void show()const; //函数原型
void man::show()const //函数定义,const后置于小括号之后
{
using namespace std;
cout << name << " is " << year << " years old." << endl;
}
这样的话,假如某个对象在声明的时候被const所限定,那么他依然可以执行show()函数,但是无法执行其他函数(假如之前那段多文件代码只改了以上这些的话)。
如代码:
const man b; //使用默认析构函数
b.show(); //调用类方法,输出对象的值
①假如你需要修改对象的值,那么就不要把其声明为const对象;
②假如某个类成员函数不会修改成员对象的值,那么应该将其声明为被const关键字所限定的函数(方法是const后置于小括号后);
③假如你声明了一个const类对象,那么就只能使用那些被const所限定的类成员函数。例如:b.update(); 是不能通过编译的。