4. 方法
4.1 构造函数
4.1.1. 语法
类名(参数){
函数体
}
特点
- 在对象被创建时自动执行
- 构造函数的函数名与类名相同
- 没有返回值类型、也没有返回值
- 可以有多个构造函数
调用时机
- 对象直接定义创建–构造函数不能被显式调用
- new动态创建
默认构造函数
- 类中没有显式的定义任何构造函数,编译器就会自动为该类型生成默认构造函数,默认构造函数没有参数。
构造函数的三个作用
- 给创建的对象建立一个标识符
- 为对象数据成员开辟内存空间
- 完成对象数据成员的初始化
初始化列表
语法
类名(参数):成员变量(参数){
函数体
}
作用
初始化非静态成员变量
说明
从概念上来讲,构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。
- 必须使用初始化列表的情况
1.常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面。
2.引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面。
3.没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。 - 初始化列表与构造函数内部成员变量赋值的区别
成员变量初始化与成员变量赋值
能使用初始化列表的时候尽量使用初始化列表
- 案例:student.cpp
#include <iostream>
using namespace std;
class Student{
private:
string name;
bool sex;
int age;
public:
Student():name(),sex(true),age(0){} // 定义默认构造函数
Student(string n,bool s,int a):name(n),sex(s),age(a){}
void Print(){
cout << "name:" << name << endl;
cout << "sex:" << (sex?"men":"women") << endl;
cout << "age" << age << endl;
}
};
int main(){
Student Tom("Tom",true,21);
Tom.Print();
Student* Emma = new Student("Emma",false,22);
Emma->Print();
Student Jack; // 像这种没有加参数的对象定义,是调用的默认构造函数
// 如果类里面定义其他的构造函数,但是没有定义默认构造函数,那么默认构造函数不能使用
Jack.Print();
}
成员变量的初始化顺序
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
C++的函数可以增加默认参数。
写法:
- 默认参数必须写在函数声明中,不能写在函数实现的参数列表中中。
- 默认参数必须写在所有非默认参数的后面。
- 默认参数可以写任意多个。
使用:
- 默认参数可以不用传递值,此时,使用默认值。
- 默认参数可以传值,此时,使用实参值。
- 案例:Bill.cpp
#include <iostream>
#include <vector>
using namespace std;
class Record{
private:
string name;
int count;
float price;
float off;
public: // 构造函数+初始化列表
Record(string name,int count,float price):name(name),count(count),price(price),off(1){}
// 构造函数重载
Record(string name,int count,float price,float off):name(name),count(count),price(price),off(off){}
float GetTotal(){
return count*price*off;
}
float GetOff(){
return count*price*(1-off);
}
void Print(){
cout << name << '\t' << count << "\t¥" << price << "\t¥" << GetTotal() << "\t¥" << GetOff() << endl;
}
};
class Bill{
private:
vector<Record> records;
public:
void Add(Record r){
records.push_back(r);
}
void Print(){
cout << "物品\t数量\t单价\t总价\t节省\n";
cout << "---------------------------------------" << endl;
float totalPrice = 0;
float totalOff = 0;
for(int i=0;i<records.size();++i){
records[i].Print();
totalPrice+=records[i].GetTotal();
totalOff+=records[i].GetOff();
}
cout << "---------------------------------------" << endl;
cout << "总价:¥" << totalPrice << "\t" << "总节省:¥" << totalOff << endl;
}
};
int main(){
Bill b;
b.Add(Record("苹果",3,3.5)); // 匿名函数
b.Add(Record("橘子",4,5.5));
b.Add(Record("梨",2,1.5,0.7));
b.Add(Record("香蕉",4,4.5,0.8));
b.Print();
}
4.2 析构函数
语法:
~类名(){
函数体
}
- 析构函数的函数名与类名相同
- 函数名前必须有一个~
- 没有参数
- 没有返回值类型、也没有返回值
- 只能有一个析构函数
- 析构函数的执行顺序与声明顺序完全相反,最先构造的最后析构
调用时机
- 对象离开作用域
- delete
默认析构函数
类中没有显式的定义析构函数,编译器就会自动为该类型生成默认析构函数
作用
释放对象所申请占有的资源
RAII(资源的取得就是初始化,Resource Acquisition Is Initialization)
C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。 – 百度百科
分析下面程序执行结果:
#include <iostream>
using namespace std;
class Test{
public:
Test(){
cout << "Test Construct" <<endl;
}
~Test(){
cout << "Test Deconstruct" << endl;
}
};
int main(){
// 局部对象
cout << "Before {" << endl;
{
cout << "After {" << endl;
Test t;
cout << "Before }" << endl;
}
cout << "After }" << endl;
// 动态对象
cout << "Before {" << endl;
{
cout << "After {" << endl;
Test* pt = new Test;
delete pt;
pt = NULL;
cout << "Before }" << endl;
}
cout << "After }" << endl;
return 0;
}
问题: new/delete与malloc()与free()的区别?
new 来定义一个对象的时候会自动的调用这个对象对应的构造函数,delete 会自动的调用这个对象的析构函数。而 malloc() 和 free() 不会。
4.3 引用(别名)
4.3.1 语法
声明:const 类型名& 对象名/类型名& 对象名
使用:与对象变量、基本类型变量一样
① 引用的用法:
int& b = a;
②定义好变量,后面的&表示取地址符号;
cout << "&b:" << &b << endl;
③引用其实就是一个别名,a与b代表的是相同的对象
- 案例:reference.cpp
#include <iostream>
using namespace std;
int main(){
int a(10);
int& b = a; // 定义引用
cout << "&a:" << &a << "\t" << a << endl; // &取地址
cout << "&b:" << &b << "\t" << b << endl;
a = 100;
cout << "&a:" << &a << "\t" << a << endl;
cout << "&b:" << &b << "\t" << b << endl;
b = 1000;
cout << "&a:" << &a << "\t" << a << endl;
cout << "&b:" << &b << "\t" << b << endl;
}
4.3.2 注意
- 定义的时候必须初始化,即给定变量;
int m;
int& b;
b = m; //错误1;
- 引用需要用变量进行引用,常量不可以引用;
int m;
int& b = 10; //错误2
- 一个变量可以有多个引用,但是一个引用只能对应一个变量,不能修改为其他变量的别名;
int& b = m;
int& b = n; //错误3
4.3.3 何处使用引用
- 避免空指针,取代指针的用法
(1.1)避免空指针,取代指针的用法一:传递参数的三种方式(Swap.cpp):
①值传递(不能修改地址的内容)
#include <iostream>
using namespace std;
void Swap(int m, int n){
int t = m;
m = n;
n = t;
}
int main(){
int n = 10;
int m = 100;
Swap(n,m);
cout << "n:" << n << "\tm:" << m << endl;
}
②指针传递(可以修改地址的内容,但是解引用比较麻烦)
#include <iostream>
using namespace std;
void Swap(int *m, int *n){
int t = *m; //t表示变量,而非指针;
*m = *n;
*n = t;
}
int main(){
int n = 10;
int m = 100;
Swap(n,m);
cout << "n:" << n << "\tm:" << m << endl;
}
③引用传递(避免空指针,野指针)
#include <iostream>
using namespace std;
void Swap(int &m, int &n){
int t = m;
m = n;
n = t;
}
int main(){
int n = 10;
int m = 100;
Swap(n,m);
cout << "n:" << n << "\tm:" << m << endl;
}
(1.2)取代指针的用法二:多个返回值
#include <iostream>
using namespace std;
int Divide(int n , int m, int& mod){
int div = n/m;
mod = n%m;
return div;
}
int main(){
int m(101);
int n(10);
int mod;
int div = Divide(m,n,mod);
cout << "div:" << div << "\tmod:" << mod << endl;
}
- 函数参数列表
- 函数返回值
- 成员变量 – 对象初始化时,必须显示初始化的变量
- 引用的三个特点
6. 引用必须初始化
7. 引用必须使用变量初始化
8. 引用初始化后不能修改,只能是一个变量的别名,不能再修改成其他变量的别名
- 案例:reference_class.cpp
#include <iostream>
using namespace std;
class Simple{
private:
int n;
public:
Simple(int n):n(n){}
void Print(){
cout << "&n:" << &n << "\t" << n << endl;
}
};
class Simple2{
private:
int* n;
public:
Simple2(int* n):n(n){}
void Print(){
cout << "n:" << n << "\t" << *n << endl;
}
};
class Simple3{
private:
int& n;
public:
Simple3(int& n):n(n){} // 引用成员变量必须在构造函数初始化列表中初始化
void Print(){
cout << "&n" << &n << "\t" << n << endl;
}
};
int main(){
Simple s(10);
s.Print();
int m = 10;
cout << "&m:" << &m << "\t" << m << endl;
// 指针
Simple2 s2(&m);
s2.Print();
m = 13;
s2.Print();
// 引用
Simple3 s3(m);
s3.Print();
}
4.3.4 为何使用引用
- 避免对象复制
- 避免传递空指针
- 使用方便
类型:
基本类型(bool、char、int、short、float)
复合类型(指针、数组、引用)
自定义类型(struct、union、class)
引用与指针的区别
- 指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
- 引用只能在定义时被初始化一次,之后不可变;指针可变;
- 引用不能为空,指针可以为空;
- 引用使用时无需解引用*,指针需要解引用;
- sizeof 引用 得到的是所指向的变量/对象的大小,而sizeof 指针得到的是指针本身的大小;
引用通常用于两种情形:成员变量和函数的参数列表以及函数返回值。
- 案例 reference_size.cpp
#include <iostream>
using namespace std;
class Types{
int m;
char c;
bool b;
float f;
double d;
};
class Pointors{
int* pm;
char* pc;
bool* pb;
float* pf;
double* pd;
};
class References{
// 在结构体类型或者类类型中,在计算类型/对象时引用类型的成员变量大小与指针一样
// 在计算单独引用类型成员变量时,引用成员大小与所引用的类型大小一致。
int& fm;
char& fc;
bool& fb;
float& ff;
double& fd;
public:
References(int& m,char& c,bool& b,float& f,double& d):fm(m),fc(c),fb(b),ff(f),fd(d){}
void PrintSize(){
cout << "sizeof(fm):" << sizeof(fm) << endl;
cout << "sizeof(fc):" << sizeof(fc) << endl;
cout << "sizeof(fb):" << sizeof(fb) << endl;
cout << "sizeof(ff):" << sizeof(ff) << endl;
cout << "sizeof(fd):" << sizeof(fd) << endl;
}
};
int main(){
int m;// 4
char c; // 1
bool b; // 1
float f;// 4
double d;//8
// 各种类型引用的大小
int& fm = m;
char& fc = c;
bool& fb = b;
float& ff = f;
double& fd = d;
cout << "sizeof(fm):" << sizeof(fm) << endl;
cout << "sizeof(fc):" << sizeof(fc) << endl;
cout << "sizeof(fb):" << sizeof(fb) << endl;
cout << "sizeof(ff):" << sizeof(ff) << endl;
cout << "sizeof(fd):" << sizeof(fd) << endl;
// 各种类型指针的大小
int* pm = &m;
char* pc = &c;
bool* pb = &b;
float* pf = &f;
double* pd = &d;
cout << "sizeof(pm):" << sizeof(pm) << endl;
cout << "sizeof(pc):" << sizeof(pc) << endl;
cout << "sizeof(pb):" << sizeof(pb) << endl;
cout << "sizeof(pf):" << sizeof(pf) << endl;
cout << "sizeof(pd):" << sizeof(pd) << endl;
cout << "sizeof(Types):" << sizeof(Types) << endl;
cout << "sizeof(Pointors):" << sizeof(Pointors) << endl;
cout << "sizeof(References):" << sizeof(References) << endl;
References r(m,c,b,f,d);
r.PrintSize();
}
- 结果显示
sizeof(fm):4
sizeof(fc):1
sizeof(fb):1
sizeof(ff):4
sizeof(fd):8
sizeof(pm):8
sizeof(pc):8
sizeof(pb):8
sizeof(pf):8
sizeof(pd):8
4.4 拷贝/复制构造函数
语法:
类名(类名& 形参){
函数体
}
或者:
类名(const 类名& 形参){
函数体
}
调用时机
手动调用
类名 对象名; // 调用默认构造函数
类名 对象2 = 对象1; // 调用复制构造函数
类名 对象3(对象1); // 调用复制构造函数
自动调用
- 一个对象作为函数参数,以值传递的方式传入函数体
- 一个对象作为函数返回值,以值从函数返回
- 一个对象用于给另外一个对象进行初始化(赋值初始化)
实践说明:
- gcc/clang自动RVO/NRVO优化(匿名的返回值优化/有名字的返回值优化),不执行拷贝构造函数,会将两次拷贝构造省掉,相当于把局部对象的生存周期拉到了外面,延长了生存周期。可以在编译命令添加选项禁止 -fno-elide-constructors; (意思是不忽略拷贝构造函数)
- VC在Debug环境下返回值执行拷贝构造函数,在Release(发布版)环境下实施RVO/NRVO优化。
-实例(调用时机测试)
#include <iostream>
using namespace std;
class Simple{
public:
Simple(){
cout << this << " Default Construction" << endl;
}
Simple(const Simple&){
cout << this << " Copy Construction" << endl;
}
~Simple(){
cout << this << " Destruction" << endl;
}
};
void Func(Simple simple){}
Simple Func(){
Simple s; // 使用默认构造函数定义带名字的对象时,对象名后最好不要加(),因为在一些旧的g++版本中会报错,报错的原因是将整行的语句Simple s();看作是一个函数的声明
return s; // 匿名对象,这种写法很像在调用默认构造函数,实际只是定义一个对象而不是调用,并且()不能去掉
}
int main(){
Simple s;
/*
Simple t = s;
Simple k(s); // Simple k = s;
*/
// Func(s);
Simple t = Func();
}
4.5 默认拷贝构造函数
- 本质:内存拷贝
- 作用:复制一个已经存在的对象
问题
- 一个类可以多个拷贝构造函数吗?
答:可以有多个。 - 拷贝构造函数的参数可以不是引用吗?例如Test(Test t)。
答:不可以,必须加引用Test(const Test& t)。 - 如何禁用默认拷贝构造函数?
答:(1)在类定义,添加私有的拷贝构造函数的声明,但不实现
private:
Test(const Test&);
(2)在C++11中
Simple(const Simple&) = delete;
下列程序的执行结果
#include <iostream>
#include <string>
using namespace std;
class Test {
public:
Test() {}
Test(Test &t) { cout << "Test(Test &t)" << endl; }
Test(Test const &t) { cout << "Test(Test const &t)" << endl; }
};
int main() {
Test t1;
Test t2 = t1;
const Test t3;
Test t4 = t3;
}
5. 赋值运算符重载函数
语法
类名& operater=(const 类名& 形参){
// 赋值操作
return *this;
}
调用时机
- 赋值
默认赋值运算符重载函数
- 内存拷贝
作用
- 赋值
如何禁用默认赋值运算符重载函数?在类定义,添加私有的赋值运算符重载函数的声明,但不实现。
拷贝构造函数与赋值操作符的区别
- 拷贝构造函数:当一个已经存在的对象来初始化一个未曾存在的对象
- 赋值操作符:当两个对象都已经存在
案例
#include <iostream>
using namespace std;
class Simple{
public:
Simple():p(NULL){
cout << this << " Default Construction" << endl;
}
Simple(const Simple& s){
p = new int(*(s.p)); // 深拷贝
cout << this << " Copy Construction : " << p << endl;
}
Simple(int n){
p = new int(n);
cout << this << " Construction : " << p << endl;
}
// 赋值运算符重载
Simple& operator=(const Simple& s){
if(this == &s) return *this; // 1.判断是否是自身复制
if(NULL != p){ // 2.必要时,删除之前申请的内存,防止内存泄漏
delete p;
}
p = new int(*(s.p));
cout << this << " operator= : " << p << endl;
return *this; // 3.返回当前对象
}
~Simple(){
cout << this << " Destruction : " << p << endl;
}
private:
int* p;
};
int main(){
Simple s(10);
Simple k = s;
Simple t;
t = s; // 赋值
}
6. 深拷贝(Memberwise Copy)与浅拷贝(Bitwise Copy)
- 浅拷贝:编译器默认生成的类实例间拷贝行为,对带有指针的类来说会引发 memory leak。
- 深拷贝:用户定义的行为(实质是一种构造函数)。
问题
一个类中存在指针类型的成员变量。并且类构造和析构管理指针的申请与释放。
class Demo{
public:
Demo(int n){
p = new int(n);
}
~Demo(){
delete p;
p = NULL;
}
private:
int* p;
};
调用拷贝构造函数会出现什么情况?
Demo a(10);
Demo b = a;
- 案例 copy_copystruct2.cpp
#include <iostream>
using namespace std;
class Simple{
int* p;
public:
Simple(int n){
cout << "Simple Construction" << endl;
p = new int(n);
}
// 拷贝构造函数
// 在函数对象按值传递时,会自动调用拷贝构造函数
Simple(Simple& s){
cout << "Simple Copy Construction" << endl;
// p = s.p; // 动态分配的内存只拷贝内存地址就是浅拷贝,危害:二次/多次释放
// 解决浅拷贝问题的方法:深拷贝
// 重新申请内存,把数据完全拷贝到新的内存
p = new int;
*p = *(s.p);
}
~Simple(){
cout << "Simple Destruction free:" << p << endl;
delete p;
p = NULL;
}
void Print(){
cout << *p << endl;
}
};
void Print(Simple simple){
simple.Print();
}
int main(){
Simple s(100);
Print(s);
}
区别
- 浅拷贝:只拷贝指针地址
- 深拷贝:重现分配堆内存,拷贝指针指向内容
最佳实践
三大定律(Rule of three / the Law of The Big Three / The Big Three)
如果类中明确定义下列其中一个成员函数,那么必须连同其他二个成员函数编写至类内,即下列三个成员函数缺一不可:
- 析构函数(destructor)
- 复制构造函数(copy constructor)
- 复制赋值运算符(copy assignment operator)
案例
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char* str;
public:
String():str(NULL){
str = new char[1];
str[0] = '\0'; // 空串
}
String(const char* s){
str = new char[strlen(s)+1];
strcpy(str,s);
}
String(const String& s){
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
}
String& operator=(String& s){
if(&s == this) return *this; // 1. 判断是否是自身赋值
delete [] str; // 2. 删掉原来申请的内存
str = new char[strlen(s.str)+1]; // 3. 重新分配内存
strcpy(str,s.str);
return *this; // 4. 返回对象引用
}
~String(){
cout << "delete " << (void*)str << endl;
if(NULL!=str) delete [] str;
str = NULL;
}
void Print(){
cout << str << endl;
}
};
int main(){
String str1;
str1.Print();
String str2("Hello");
str2.Print();
String str3(str2); // 拷贝构造函数
str3.Print();
str1 = str2; // 赋值运算符重载函数
str2.Print();
str2 = str2; // 自我赋值特例
str2.Print();
}