一些小细节

在c++中,struct的功能已经进行扩展,基本和class的功能一致
唯一的区别就在于struct默认为public,class默认为private

对象的创建

默认创建

如果不需要任何附加条件,仅仅是创建一个对象的话,直接写是OK的
对象创建时,如果是带括号的无参创建,实际意义是函数声明(很奇怪,在函数内部还能声明函数)

#include <iostream>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
void setbrand(const string& brand)
{
_brand = brand;
}
void setprice(const int& price)
{
_price = price;
}

private:
string _brand;
int _price;
};

int main()
{
Computer p1;
return 0;
}

函数成员声明再定义

函数成员可以像外部函数一样声明后再在类外部进行定义
但此时会有一个缺点,我们想要通过函数成员对数据成员进行修改时,数据成员不允许访问,所以一般的,如果想要在类外部定义函数成员,就需要像namespace的引用方法一样去定义

#include <iostream>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
void setbrand(const string& brand);
void setprice(const int& price);
private:
string _brand;
int _price;
};

//类外部定义函数成员,通过作用域来引用,此时可以访问数据成员
void Computer::setbrand(const string& brand)
{
_brand = brand;
}
void Computer::setprice(const int& price)
{
_price = price;
}

int main()
{
Computer p1;
return 0;
}

对象初始化

构造函数

构造函数就是用来初始化数据成员的
形式:没有返回值,非void,函数名与类名必须相同

默认构造函数

由系统提供的一个构造函数,因此即使对象的创建过程没有构造函数也是允许的

构造函数定义

#include <iostream>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:

//构造函数一般写法
Computer(const string& brand, const int& price)//建议取地址进行引用,可以节省内存占用
{
_brand = brand;
_price = price;

cout << "Have done structure!" << endl;
}
private:
string _brand;
int _price;
};

void test0()
{
Computer p1("HUAWEI", 4999);//因为有了显示构造函数,对象的创建也会发生改变
}
int main()
{
test0();

return 0;
}

上述方法只是一般构造函数写法,赋值并非是真正对数据成员初始化

构造函数初始化对象

对于类中数据成员的初始化,需要初始化列表进行完成
形式:在构造函数的头与构造函数的体之间,用 ‘ : ‘ 进行分隔

Computer(const string& brand, const int& price)
:_brand(brand)//如果brand为字符数组,可以在函数体内部通过strcpy或者memset对brand初始化
, _price(price)//如果数据成员有多个,为了代码规范,就另起一行并以逗号分隔
{
cout << "Have done structure!" << endl;
}

初始化数据成员的顺序与初始化列表无关,如果有两个数据成员ix和iy,当已经把ix初始化了后,用ix去初始化iy,iy的值仍为随机值

对象的销毁

析构函数

用于对象的销毁,系统会默认提供
形式:同构造函数一样,没有返回值
没有参数,函数名与类名相同,但前面带个 ‘~’,且析构函数只能有一个

一般形式

#include <iostream>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
Computer(const string& brand, const int& price)
:_brand(brand)
, _price(price)
{
cout << "Have done structure!" << endl;
}
~Computer()
{
cout << "Have done Delete" << endl;
}
private:
string _brand;
int _price;
};

void test0()
{
Computer p1("HUAWEI", 4999);
p1.~Computer();

}
int main()
{
test0();

return 0;
}

输出结果

Have done structure!
Have done Delete
Have done Delete

可以看到输出了两次销毁,因此可以断定对象的销毁一定调用了析构函数,但调用析构函数并不一定代表对象的销毁

不同类型对象销毁的次序

一般来说,对象的销毁时间与其生命周期有关
全局对象生命周期最长,在程序结束时才能销毁
静态对象在main函数结束时进行销毁
局部对象以及语句块中的变量在函数或代码块结束时进行销毁
堆对象在delete或者free时进行销毁,如果不写就会造成内存泄漏

#include <iostream>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
Computer(const string& brand, const int& price)
:_brand(brand)
, _price(price)
{
//cout << "Have done structure!" << endl;
}
~Computer()
{
cout<<_brand<<' '<< "Have done Delete" << endl;
}
private:
string _brand;
int _price;
};

Computer p4("All", 4);

void test0()
{
Computer p1("temp", 1);//局部变量
static Computer p3("static", 3);
}
int main()
{
test0();

Computer p2("main", 2);
Computer* p5 = new Computer("new", 5);

delete p5;

return 0;
}

输出结果

temp Have done Delete
new Have done Delete
main Have done Delete
static Have done Delete
All Have done Delete

析构函数的注意事项

在Computer类中, 我们用string定义了_brand数据成员
如果非要采用字符数组的形式进行定义,会出现不确定数组大小是否合适的问题
改用指针的形式,通过new或者malloc分配空间可以解决该类问题

#include <iostream>
#include <cstring>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
Computer(const char* brand, const int& price)
:_brand(new char[strlen(brand)+1]())//通过new分配空间,后面加的小括号可以初始化_brand
, _price(price)
{
strcpy(_brand, brand);//初始化brand
//cout << "Have done structure!" << endl;
}
~Computer()
{
cout<<_brand<<' '<< "Have done Delete" << endl;
}
private:
char* _brand;
int _price;
};

此时又会出现新的问题,我们无法确定对象在销毁时,new分配的空间是否会被delete掉,通过内存检测工具发现是没有销毁掉的,因此我们需要对析构函数进行改进,而这一情况的讲解我会放在拷贝函数之后

对象的拷贝

如果未显示定义,系统会默认提供一个拷贝函数
类中定义的固定形式

//Point类名

class Point
{
public:
Point(const Point& p)
: _ix(p._ix)
,_iy(p._iy)
{
;
}
private:
int _ix;
int _iy;
}

对象拷贝时的两种情况

#include <iostream>
#include <cstring>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
Computer(const char* brand, const int& price)
:_brand(new char[strlen(brand)+1]())//通过new分配空间,后面加的小括号可以初始化_brand
, _price(price)
{
strcpy(_brand, brand);//初始化brand
//cout << "Have done structure!" << endl;
}
~Computer()
{
cout<<_brand<<' '<< "Have done Delete" << endl;
}

void print()
{
cout << _brand << ' ';
cout << _price << endl;
}
private:
char* _brand;
int _price;
};

void point(Computer p)
{
p.print();
}

Computer retu()
{
Computer pt("xx", 11);
return pt;
}
void test1()
{
Computer p1("1", 1);

//对象未被创建时
Computer p2 = p1;
Computer p3(p1);
//对象已被创建时
//一般形式
Computer p4("2", 2);
p4 = p1;
//实参传给形参
point(p1);
//返回值为对象
}
int main()
{
test1();

return 0;
}

通过直接赋值的方式进行拷贝是浅拷贝,可以通过对赋值运算符重构使之成为深拷贝,同时注意函数传参如果采用实参传形参而不是引用的形式,那么传参的拷贝是浅拷贝

Computer& operator = (const Computer& p)
{
Computer tmp("", 1);
tmp._brand = new char[strlen(p._brand) + 1]();
tmp._price = p._price;
strcpy(tmp._brand, p._brand);

cout<<tmp._brand<<' '<<"Have done copy"<<endl;

return tmp;
}

构造拷贝函数时的两个注意点

1.为什么要使用&

如果不去使用&符号,函数形式会变成Computer(const Computer p)
可以发现如果这么做,参数部分就是创建对象的形式,他会像一个栈递归一样无限创建下去,直到内存爆满
又因为进行了指针访问,可以认为形参对象就在类的内部,可以访问数据成员
使用指针也可以代替&

2.为什么需要const修饰

首先需要了解左值和右值的概念
右值:没有写入内存的数据,不能进行取地址访问
左值:以及写入内存的数据,可以进行取地址访问
值得注意的是,函数如果有返回值,那么他的返回值就是临时变量,这里的临时变量就是右值,无法取地址访问
而通过const修饰,可以绑定右值

int a1 = 1;
int& a2 = a1;
int& a3 = 1;
const int& a4 = 1;

可以发现带有a3无法编译通过,去掉a3后,a1a2a4是可以编译通过的,因为1是右值,无法进行取地址访问,而通过const修饰绑定右值后就可以强行访问了
同理,函数返回值也是如此

class Computer
{
public:
Computer(const char* brand, const int& price)
:_brand(new char[strlen(brand)+1]())//通过new分配空间,后面加的小括号可以初始化_brand
, _price(price)
{
strcpy(_brand, brand);//初始化brand
//cout << "Have done structure!" << endl;
}
~Computer()
{
cout<<_brand<<' '<< "Have done Delete" << endl;
}
Computer(const Computer& p)
:_brand(p._brand)//此时采用地址赋值的形式,会在后文解释改进方法
,_price(p._price)
{
cout<<_brand<<' '<<"Have done copy"<<endl;
}
void print()
{
cout << _brand << ' ';
cout << _price << endl;
}
private:
char* _brand;
int _price;
};

Computer retu()
{
Computer pt("xx", 11);
return pt;
}
void test2()
{
Computer a1("1",1);
Computer a2 = retu();
}

如果在拷贝函数定义过程中去掉了const修饰,a2是无法通过的,通过const绑定右值后即可进行

关于拷贝函数和析构函数的创建时机

前文在析构函数中提到,如果采用字符数组的形式以new分配空间, 在对象销毁时会发生内存泄漏
此时我们选择对析构函数进行改进

void release()
{
delete[] _brand;
_brand = nullptr;//c++11中更安全的置空方法
}
~Computer()
{
release();
cout<< "Have done Delete" << endl;
}

此时我们可以避免内存发生泄漏
我们通过sizeof查看类的大小时往往不是我们想要的结果,比如_brand为11个字节时,再加上int时,该类的大小实际却为16,这是因为一种对齐数的存在,在64位系统下类和结构体的大小必须为8的倍数,32位系统下必须为4的倍数

在拷贝函数中我们注意到,当时我们只是采用地址直接赋值的方式对新的对象复制
往往会发生以下情况

void test3()
{
Computer p1("HUAWEI", 4999);
Computer p2 = p1;
p1.~Computer();
p2.print();
}

此时我们发现在程序运行过程中发生崩溃,这是因为字符数组我们仅仅复制了地址过去,p1在销毁后,p2中brand的地址仍是p1的brand,brand的值为NULL,进行了非法访问
改进方法,在拷贝函数中,不再是复制地址,而是另开辟一个新的空间存放brand,通过strcpy将brand复制过去

Computer(const Computer& p)
:_brand(new char[strlen(p._brand) + 1]())
,_price(p._price)
{
strcpy(_brand, p._brand);

cout<<_brand<<' '<<"Have done copy"<<endl;
}

全过程代码

#include <iostream>
#include <cstring>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
Computer(const char* brand, const int& price)
:_brand(new char[strlen(brand)+1]())//通过new分配空间,后面加的小括号可以初始化_brand
, _price(price)
{
strcpy(_brand, brand);//初始化brand
//cout << "Have done structure!" << endl;
}
void release()
{
delete[] _brand;
_brand = nullptr;//c++11中更安全的置空方法
}
~Computer()
{
if(_brand) release();
cout<< "Have done Delete" << endl;
}
Computer(const Computer& p)
:_brand(new char[strlen(p._brand) + 1]())
, _price(p._price)
{
cout << _brand << ' ' << "Have done copy" << endl;
}
//对赋值运算符重构
Computer& operator = (const Computer& p)
{
Computer tmp("", 1);
tmp._brand = new char[strlen(p._brand) + 1]();
tmp._price = p._price;
strcpy(tmp._brand, p._brand);

cout<<tmp._brand<<' '<<"Have done copy"<<endl;

return tmp;
}
void print()
{
cout << _brand << ' ';
cout << _price << endl;
}
private:
char* _brand;
int _price;
};

Computer p44("All", 4);

void test0()
{
Computer p1("temp", 1);//局部变量
static Computer p3("static", 3);
}

void point(Computer p)
{
p.print();
}

Computer retu()
{
Computer pt("xx", 11);
return pt;
}
void test1()
{
Computer p1("1", 1);

//对象未被创建时
Computer p2 = p1;
Computer p3(p1);
//对象已被创建时(注意对象已存在时直接赋值为浅拷贝)
//一般形式
Computer p4(p1);
p4 = p1;
//实参传给形参
point(p1);
//返回值为对象
p4 = retu();
}

void test2()
{
Computer a1("1",1);
Computer a2 = retu();
}
void test3()
{
Computer p1("HUAWEI", 4999);
Computer p2(p1);
p1.~Computer();
p2.print();
}
int main()
{
test0();

Computer p2("main", 2);
Computer* p5 = new Computer("new", 5);
delete p5;

test1();

test2();

test3();

return 0;
}

对象的现代写法
深拷贝采用swap的优化方式,而浅拷贝不允许,解释:在函数实参传形参过程中,采用了浅拷贝,当前对象未被创建过,swap后临时变量地址未知,delete时会出现问题

#include <iostream>
#include <cstring>
#include <algorithm>

using std::cout;
using std::endl;
using std::string;

class Computer
{
public:
Computer(const char* brand, const int& price)
:_brand(new char[strlen(brand) + 1]())//通过new分配空间,后面加的小括号可以初始化_brand
, _price(price)
{
strcpy(_brand, brand);//初始化brand
cout << "Have done structure!" << endl;
}
void release()
{
//cout << &_brand << endl;
delete[] _brand;
_brand = nullptr;//c++11中更安全的置空方法
}
~Computer()
{
if (_brand) release();
cout << "Have do ne Delete" << endl;
}

void swap(Computer& p)
{
std::swap(this->_brand, p._brand);
std::swap(this->_price, p._price);
}

Computer(const Computer& p)
:_brand(new char[strlen(p._brand) + 1]())
,_price(p._price)
{
strcpy(_brand, p._brand);
}
Computer& operator = (const Computer& p)
{
if (this == &p) return *this;

Computer tmp(p._brand, p._price);

swap(tmp);

return *this;//返回类自己
}

void print()
{
cout << _brand << ' ';
cout << _price << endl;
}
private:
char* _brand;
int _price;
};

Computer p44("All", 4);

void test0()
{
Computer p1("temp", 1);//局部变量
static Computer p3("static", 3);
}

void print(Computer a)
{
a.print();
}

Computer retu()
{
Computer pt("xx", 11);
return pt;
}
void test1()
{
Computer p1("1", 1);

//对象未被创建时
Computer p2 = p1;
Computer p3(p1);
//对象已被创建时(注意对象已存在时直接赋值为浅拷贝,使用重构改为深拷贝)
//一般形式
Computer p4(p1);
p4 = p1;
//实参传给形参,浅拷贝
print(p1);
//返回值为对象
p4 = retu();
}

void test2()
{
Computer a1("1", 1);
Computer a2 = retu();
}
void test3()
{
Computer p1("HUAWEI", 4999);
Computer p2(p1);
p1.~Computer();
p2.print();
}
int main()
{
//test0();

Computer p2("main", 2);
Computer* p5 = new Computer("new", 5);
delete p5;

test1();

//test2();

//test3();

return 0;
}