程序编译和运行时
所谓程序编译就是其实就是翻译的过程,翻译成为01二进制代码,但是呢,例如java语言你翻译之后能让jvm运行的底层01.编译就是检查检查语法错误,好能够满足语法要求,可以运行.
所谓运行,就是编译程序正确,装载到内存中,要开始跑起来了,这才开始分配内存什么的,比如malloc,比如new之类的,都是要程序运行起来才能完成的功能.
内存负载
所谓的内存负载就是浪费大量的内存,比如说有一个可以存40个字符的字符数组,我用了两千个数组,但是每个数组我只用了1个字符大小的空间
笑出声音…
这时候就是内存负载了,所以说如果在程序运行的时候让程序自己判断用了多少内存,每一次都是动态new出空间,就会完美的利用空间.
动态内存分配错误使用
再谈析构函数,c++使用new和delete来控制内存,如果在类中使用这些new和delete,就会导致很多新的编程问题,这就需要重新来审视析构函数的重要性.下面定义一个字符串的类
class StringBad
{
private:
char * str; // 指向字符串首字符
int len; // 字符串长度
static int num_strings; // 增加的对象数量
public:
StringBad(const char * s); // 带参数的构造函数
StringBad(); // 默认构造函数
~StringBad(); // 析构函数
// 友元函数
friend std::ostream & operator<<(std::ostream & os,const StringBad & st);
};
静态存储类中的数据num_strings,代表着无论声名了多少个变量,永远只有一个num_strings.比如说有10个StringBad对象,就有10个str指针,也有10个len整形变量,但是永远只有一个num_strings变量
成员函数实现如下,不能再类中初始化静态成员变量,类声名只是如何描述分配内存,而不真正的分配内存
// 初始化静态数据
int StringBad::num_strings = 0;
// 类方法
StringBad::StringBad(const char * s)
{
len = std::strlen(s); // 设定大小
str = new char[len + 1]; // 分配内存
std::strcpy(str, s); // 将s所指字符串复制给str
num_strings++; // 对象个数+1
cout << num_strings << ": \"" << str
<< "\" object created\n"; // 打印出有多少个对象创建
}
StringBad::StringBad() // 默认构造函数
{
len = 4;
str = new char[4];
std::strcpy(str, "C++"); // 没有初始化对象,默认值
num_strings++;
cout << num_strings << ": \"" << str
<< "\" default object created\n"; // 打印出有多少个对象创建
}
StringBad::~StringBad() // 析构函数
{
cout << "\"" << str << "\" object deleted, "; // 某个字符串被删除
--num_strings; // 对象数量减一
cout << num_strings << " left\n"; // 剩多少对象留下
delete [] str; // 释放空间
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
> 字符串本身就是一个指针,类似于字符数组的名字,一个字符串"string"的值是不可能变动的,也就是说字符串是地址,是个常量,所以用const char * s来设置参数,而且不能用指针赋值的方式来创建字符串,例如:
StringBad::StringBad(const char * s)
{
len = std::strlen(s); // 设定大小
str = new char[len + 1]; // 分配内存
std::strcpy(str, s); // 将s所指字符串复制给str
num_strings++; // 对象个数+1
cout << num_strings << ": \"" << str
<< "\" object created\n"; // 打印出有多少个对象创建
}
这个函数是创建一个字符串,也就是说,参数s指针就是个指针,如果使用这样的操作str = s,将s赋值str,仅仅是多了一个指针str指向了这个字符串,并没有进行把s所指字符串的值赋值给str = new char[len + 1]这个新开辟的空间,所以就要用到了stecpy( , )这个函数
main()函数内容如下
----------自定义函数-------------------
void callme1(StringBad & rsb)
{
cout << "String passed by reference:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb)
{
cout << "String passed by value:\n";
cout << " \"" << sb << "\"\n";
}
int main()
{
using std::endl;
{
----------正常初始化-------------------
cout << "Starting an inner block.\n";
StringBad headline1("Celery Stalks at Midnight"); //初始化
StringBad headline2("Lettuce Prey"); //初始化
StringBad sports("Spinach Leaves Bowl for Dollars"); //初始化
----------打印出三个对象-------------------
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
----------调用callme1-------------------
callme1(headline1);
cout << "headline1: " << headline1 << endl;
----------调用callme2,出错开始-------------------
callme2(headline2);
cout << "headline2: " << headline2 << endl;
----------创建sailor对象-------------------
cout << "Initialize one object to another:\n";
StringBad sailor = sports; //通过对象之间的赋值初始化,并没有通过构造函数
cout << "sailor: " << sailor << endl;
----------创建knot对象-------------------
cout << "Assign one object to another:\n";
StringBad knot; //通过默认构造函数初始化
knot = headline1; //然后在把headline1对象的值赋值给knot
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n";
}
cout << "End of main()\n";
}
效果如下
这个结果出现了很多错误,对此进行分析,首先要先知道几个概念
-
默认构造函数
-
复制构造函数
什么是复制构造函数,先看这句StringBad sailor = sports,把sports对象传递给sailor对象, 这个和结构体传值类似,就是把sports对象中的隐藏数据挨个复制给sailor对象,其实实质上就是调用了复制构造函数,也就是说,下面这条语句
StringBad sailor = sports;
//这条语句和StringBad sailor = StringBad(sports);等价,都是把已存的对象赋值给新对象
这条语句调用了复制构造函数原型如下(定义先忽略,只要知道他是把把sports对象中的隐藏数据挨个复制给sailor对象)
StringBad(const StringBad& );
何时调用复制构造函数?
上面已经阐述过什么时候调用,总结一下:新建一个对象并用现有的对象进行初始化操作的时候就会调用构造函数,这个在很多情况下就会发生,拿上面的例子举例,已经存在一个spotrs对象,用sports对象初始化sailor对象.
StringBad sailor = sports; //调用复制构造函数
StringBad sailor = StringBad(sports);
//和上面这条语句等价,也会调用复制构造函数,着两条语句可能通过复制构造函数直接创建新对象,也可能通过复制构造函数创建一个临时对象,这个取决于具体实现
StringBad sailor(sports); //调用复制构造函数,用已有对象初始化新对象
StringBad* pStringBad = new StringBad(sports); //调用复制构造函数,初始化一个匿名对象,并通过指针pStringBad管理
- 默认复制构造函数功能
根据上面的内容,复制构造函数如果没有自己定义,那么就会有一个默认复制构造函数来完成相应功能
默认构造函数的任务就是除了非静态成员,其他成员逐个赋值给另一个新建的对象(也可以叫浅复制),比如
StringBad sailor = sports;
与下面的代码等效(但是下面的代码不能通过编译,因为oop的特性是数据隐藏)
StringBad sailor;
sailor.str = sports.str;
sailor.len = sports.len;
运行过程如下图,默认复制函数只是把相应的值给复制过去了,比如指针str的值复制过去了,但是呢str指向的字符串中的值并没有复制过去,所以这是导致错误的原因之一.
有了上面的概念,StringBad出现的问题有两处:
-
析构函数的调用次数比构造函数多次数多两次.原因:复制构造函数用来初始化callme2()的形参,也被用来初始化对象sports,默认复制构造函数没有让num_Strings加一
-
出现乱码,主要是因为内存没有管理好.原因是默认复制函数中,复制的不是字符串而是指针的值,所以在调用析构函数的时候delete[]sailor.str就是把整块空间释放掉了,同样sports.str也被释放掉了,因为sailor中的str值就是sports中的str值.
修改StringBad,就要自己定义一个复制构造函数,增加num_Strings加一功能,增加将str所指空间的值复制过来(深度复制).
定义如下:
StringBad :: StringBad(const StringBad& st)
{
num_strings++;
len = st.len;
str = new char[len + 1];
std :: strcpy(str , st.str); //内存中的值也复制过来
cout << num_strings << ": \"" << str
<< "\" object created\n";
}
StringBad的其他问题:赋值运算符
StringBad的问题除了复制构造函数,还有赋值符号得问题如下:
- StringBad sailor = sports;这一句并不存在赋值的问题,因为这时调用的复制构造函数来完成的,各个成员都是复制构造函数中完成的.
- 类似于上面的赋值操作StringBad knot;knot = headline1;,这两条语句也是声名一个对象,然后调用了默认构造函数初始化了 knot,然后在用headline1的值赋值给了knot,与复制构造函数相似,赋值运算符也是隐式的挨个把值复制给了knot,静态数据成员不受影响.也就说这种情况和默认复制构造函数出现的情况很像.
在C++中,可以直接进行对象之间的赋值运算,这依赖于类的赋值运算符重载,这种运算符的原型如下
class_nmae& class_name :: operator=(const class_name&);
具体化:
StringBad& StringBad :: operator=(const StringBad&);
这个赋值运算符重载函数内容就是把类成员逐个复制给调用的对象,然后在返回对象.
所以需要重写这个函数才能够纠正错误(赋值运算符重载函数只能是类成员函数)
StringBad& StringBad :: operator=(const StringBad& st)
{
if(this == &st)
return *this; //查看是不是自己给自己赋值,如果是,返回自身.
delete [] str; //如果不是自我赋值,那就把之前使用构造函数初始化的空间delete掉.
len = st.len; //将st的长度赋值给调用函数对象
str = new char[len + 1]; //new出新空间
std :: strcpy(str , st.str); //将st中的字符串复制给调用函数对象的str
return *this; //最后将更新之后的自身返回
}
改善之后的StringBad类
string类如下:
class StringBad
{
private:
char * str; // 指向字符串首字符
int len; // 字符串长度
static int num_strings; // 增加的对象数量
public:
StringBad(const char * s); // 带参数的构造函数
StringBad(); // 默认构造函数
~StringBad(); // 析构函数
StringBad(const StringBad& st); //重写复制构造函数
StringBad& operator=(const StringBad& st); //重写=重载函数运算符
// 友元函数
friend std::ostream & operator<<(std::ostream & os,const StringBad & st);
};
类成员函数实现如下:
// 初始化静态数据
int StringBad::num_strings = 0;
// 类方法
StringBad::StringBad(const char * s)
{
len = std::strlen(s); // 设定大小
str = new char[len + 1]; // 分配内存
std::strcpy(str, s); // 将s所指字符串复制给str
num_strings++; // 对象个数+1
cout << num_strings << ": \"" << str
<< "\" object created\n"; // 打印出有多少个对象创建
}
StringBad::StringBad() // 默认构造函数
{
len = 4;
str = new char[4];
std::strcpy(str, "C++"); // 没有初始化对象,默认值
num_strings++;
cout << num_strings << ": \"" << str
<< "\" default object created\n"; // 打印出有多少个对象创建
}
StringBad::~StringBad() // 析构函数
{
cout << "\"" << str << "\" object deleted, "; // 某个字符串被删除
--num_strings; // 对象数量减一
cout << num_strings << " left\n"; // 剩多少对象留下
delete [] str; // 释放空间
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
StringBad :: StringBad(const StringBad& st) //重写复制构造函数
{
num_strings++;
len = st.len;
str = new char[len + 1];
std :: strcpy(str , st.str); //内存中的值也复制过来
cout << num_strings << ": \"" << str
<< "\" object created\n";
}
StringBad& StringBad :: operator=(const StringBad& st) //重写=重载函数运算符
{
if(this == &st)
return *this; //查看是不是自己给自己赋值,如果是,返回自身.
delete [] str; //如果不是自我赋值,那就把之前使用构造函数初始化的空间delete掉.
len = st.len; //将st的长度赋值给调用函数对象
str = new char[len + 1]; //new出新空间
std :: strcpy(str , st.str); //将st中的字符串复制给调用函数对象的str
return *this; //最后将更新之后的自身返回
}
效果如下:
收工…
str = new char[len + 1]; //new出新空间
std :: strcpy(str , st.str); //将st中的字符串复制给调用函数对象的str
return *this; //最后将更新之后的自身返回
}
效果如下:
[外链图片转存中…(img-VA7SDlGv-1590412050631)]
只要对复制构造函数和赋值运算符重载有了一定的认知,就能正确使用类对象使用的内存了
收工…