Week2 Notes
A.三大函数:拷贝构造,拷贝赋值,析构
string class这个不是标准库里的string,标准库里的太复杂了。
首先也要有防卫式声明。Ifndef define endif
测试代码
int main(){
strings1();
string s2(“hello”);
string s3(s1);//拷贝
cout << s3 << endl;//操作符重载
s3 = s2;//赋值动作
cout << s3<< endl;
}
上面的赋值和拷贝的不同是第一个s3是第一次出现,第二个s3是赋值。所以第一个是拷贝构造,第二个叫拷贝赋值。如果你没有写,编译器会自动给你一套,是一位一位地拷贝。那我们还要自己写一份吗?要考虑编译器给的这一套够不够用,比如复数只要给实部虚部就够用,但是这种带指针的如果还使用编译器给的拷贝构造和拷贝赋值,就不够用,深拷贝和浅拷贝????
Class string{
Public:
String(constchar* cstr = 0);
String(conststring& str);
String&operator = (const string& str);
~string();
char*get_C_str() const {return m_data};
private:
char*m_data;
}
字符串的指针要写在private里,我们要自己写一套拷贝,第一个函数函数名称和类相同,所以是构造函数,第二个函数名字也一样,也是构造函数,但它的接收类型是自己,所以是拷贝构造。第三个操作符=的输入也是string,是拷贝赋值,注意,只要你的类带着指针,你一定要写出这两个拷贝函数!!!!
第四个波浪线开头的是析构函数。当它死亡的时候析构函数就会被调用,新增加的234被成为big three,三个特殊函数。
第五个复习加的const是因为直接返回data不改变所以要加。
Ctor和dtor(构造函数和析构函数)
一个字符串多长有两种想法,一种是不知道长度但最后有结束符号,另一种是没有结束符号,但多一个lenth,长度。C和c++用的是有结束符号的设计方法。
Inline
String::string(const char* cstr = 0){
If(cstr){
M_data = new char[strlen(cstr) + 1];
Strcpy(m_data, cstr);
}
else{
m_data = new char[1];
*m_data = ‘\0’;
}
}
inline
string::~string(){
delete[]m_Data;
}
{
stirngs1(),
strings2(“hello”);
string*p = new string(“hello”);
delete p;
}
传进来先判断是不是空字符串,如果是空的分配一个字符,为结束符,如果不为空,分配的空间大小是传进来的长度还要加上一个1,结束符号\0,分配完了之后,再用strcpy拷贝进我们的m_data里。加入传进来的是hello的话,长度就是5加1,结束符号,面对字符串都要想到结束符号,新创建的对象就拥有内容了。
对应于构造函数,有下面的析构函数,做的事情是关门清理,clean up,在之前讲过的复数时,不需要清理,如果你的class里有指针,多半要用动态分配,new,在使用动态分配的时候,我们要用析构函数把动态分配的空间释放掉,否则会发生内存泄漏。
String *p = new string(“hello”);
Delete p;
动态分配后再调用。
在这个作用域里头有三个字符串,离开的时候要调用三次析构函数。
Class with pointer members必须有copy ctr和copy op=
因为如果不这么做,编译器自己做的是一个位一个位的拷贝,对于b= a来说,a的内容只有一个指针,如果不这么做,b = a就会把b里的指针也指向a指针指向的内容,这对于两个内容都很危险,对于b的内容来说,他指向的内容还在可是没有指针指向他,会发生内存泄漏,memory leak.对于a来说,两个指针指向他也很危险,一个改变了另一个也改变,所以这个叫浅拷贝,我们写的函数是为了深拷贝。
Copy ctor拷贝构造函数
Inline
String::string(const string& str){
M_data= new char[strlen(str.m_data) + 1];
Strcpy(m_Data,str.mdata);
}
{
string s1(“hello”);
string s2(s1);
}
在对m_data赋值的时候直接取另一个object的private,(兄弟之间互为friend)
在把指针拷过来的同时把内容也拷过来,叫深拷贝,只把指针拷贝过来叫浅拷贝。
Copy assignment operator拷贝赋值函数
为了把右边的东西赋值给左边,本来两边都有东西,需要先把左边清空,分配一块和右边一样大的空间,再把右边拷贝过来。
Inline
String& string::operator = (conststring& str){
If(this== &str)
Return*this;//检测自我赋值
Delete[]m_Data;
M_Data= new char[strlen(str.m_Data) + 1]
Strcpy(m_data,str.m_data);
Return*this;
}
{
string s1(“hello”);
string s2(s1);
s2 = s1;
}
自我赋值的检测判断条件是两个指针相比,自我赋值如果不写,好像也不会出错,只是效率高低吗,不是,如果不写自我赋值检测,有可能会出错。当左右两边一样的时候,赋值的第一步是杀掉左边的m_data,在做第二个动作的时候string已经不见了,会出错。所以自我赋值不只是为了效率。
B.堆栈与内存管理
Output函数,是不是可以加在自己的rectangle类里?
#include
ostream& operator <<(ostream& os, const String str){
os<< str.get_c_str();
returnos;
}
stack和heap
函数本身会形成一个stack用来防止它接收到的参数,以及返回地址。
在函数本体内的声明的任何变量,其所使用的内存块都取自上述stack。
System heap是指操作系统所提供的一块global的内存空间,程序可以动态分配从某种获得若干区块。可以在程序的任何地方动态获得,并有责任区释放他。
Class Complex();
{
complexc1(c1,c2);
complex*p= new complex(3);
}
C1叫做aotu object,stackobject;
Static complex c2(1, 2);
C2是static object,声明在作用域结束之后依然存在,它的析构函数不会再大括号结束的时候被调用,会在整个程序结束之后被调用。
还有一种对象叫全局对象,写在任何大括号之外的叫做全局对象,global object,声明在整个程序结束之后才消失,也可以视为一种static object..
{
complex*p = new Complex;
}
上面的程序会发生内存泄漏,在作用域结束后指针就死亡了,但指针指的对象还存在,就会发生memory leak;
new:先分配memory,再调用ctor;
complex *pc = new complex(1, 2);
编译器会*pc;
void * mem = operator
new(sizeof(complex));//分配内存,里面调用malloc
pc = static_cast(mem);
pc->complex::complex(1,2);
delete:先调用dtor,再释放memory
在复数的时候我们没有写析构函数,写了也是白写,马上就要死亡了。
String ps = new string(“hello”);
Delete ps;
String::~string(ps);//析构函数
Operator delete(ps);//释放内存
使用malloc和free到底分配多少内存。
Comple:两个double是8个字节,在调试模式下上面会多出32个字节,下面会多出4个字节,上下还有cookie,是4个字节。
一共有8+(32+4)+4*2 = 52
在vc下面分配的内存块一定是16字节的倍数,所以分配64个字节。
在非调试模式底下,一个复数的大小要去掉灰色的,所以是8+4*2 = 16个字节。上下cookie是用来记录整个的大小,因为在释放的时候只给一个指针,要知道释放的内存大小cookie head记录将大小的最后一位记为1.因为大小是16字节的倍数,所以最后一位肯定是1.
String内含一个指针,分配的内存大小为:
4+(32+4)+(4*2) = 48是16的倍数,在非调试模式下,大小为4+4*2 = 12,16
如果分配的是数组array会怎么样?
Array new要搭配array delete不然会出错。
Complex*p = new complex[3];
(8*3) + (32+4)+(4*2)+4 = 72
给80
非调试模式下,(8*3) + (4*2) + 4 = 36给48
如果array new没有搭配array delete,会造成内存泄漏。
String *p = new string[3];
Delete[] p;
写不写中括号都不影响指针这一整块的删除,因为分配的内存的大小就记录在cookie上,问题出在没加中括号只调用了一次dtor,只有写了中括号编译器才知道你下面是数组,会唤起三次dtor,这三个dtor负责把各自动态分配的内存杀掉。
C.复习String类的实现
动态分配的内存块。(memory block)
下面来写一个字符串的class,编程实例:
class String{
public:
String(constchar* cstr = 0);
String(constString& str);
String&operator = (const String& str);
~String();
char*get_c_str() const {return m_data;}
private:
char* m_data;
};
构造函数和析构函数
inline
String::String(const char* cstr = 0){
If(cstr){
m_data= new char[strlen(cstr) + 1];
strcpy(m_data,cstr);
}
else{//未设定初值
m_data = new char[1];
*m_data = ‘\0’;
}
}
inline
String::~String(){
delete[]m_data;
}
由于上面是用array new,所以下面用array delete。
拷贝构造函数copy cstr
inline
String::String(const String& str){
m_data= new char[strlen(str.m_data) + 1];
strcpy(m_data,str.m_data);
}
copy assignment operator拷贝赋值函数
inline
String& String::operator=(constString& str){
If(this== &str)
return*this;
delete[] m_data;
m_data = newchar[strlen(str.m_data) + 1];
strcpy(m_data,str.m_data);
return *this;
}
返回类型不是void,因为当连续=时可能会出错。
拷贝赋值要去检验是否是自我赋值,如果是自我赋值,就可以直接返回。如果没有做这种检查,不只是效率的问题,是正误的问题。在这里const String& str中的&符号和this == &str有不同的意义,前面是传引用,后面是取地址。前面是出现在typename的后面,叫引用,后面的是出现在object的前面,叫取地址,得到的是指针。
D.扩展补充,类模板,函数模板及其他
对象经典有带指针的和不带指针的。有很多细节需要补充。
进一步补充:static静态
当完成基于对象后要做面向对象,在不同的类内做时要知道this pointer
不管是数据还是函数前面都可以加static
complex c1, c2, c3;
cout << c1.real();
cout << c2.real();
c1调用real函数从另一个角度看就是
complex c1, c2, c3;
cout << complex::real(&c1);
cout << complex::real(&c2);
在没有导入static的时候,函数只有一份,但是它要来处理很多个对象,一定要有人告诉它你要处理谁,靠的就是this pointer,通过这个指针找到它要处理的对象在哪里。成员函数有一个this pointer但是我们不能写进去,这是编译器自动会帮我们写。
加了静态static后它和对象就脱离了,他在内存中有单独一部分区域,静态数。
静态函数的身份和一般的成员函数一样在内存中只有一份。静态的数据只有一份,什么时候会使用呢?比如在设计一个银行的账户体系,有一百万个人来开户,我们需要设计一百万个人的对象,但利率需要一个同一个东西,一百万个人同一个利率,此时我们需要将利率设为静态,在内存中只有一份,静态函数的特征和一般成员函数不同在它没有this pointer,这样它不能去访问去处理,那它有什么用,静态函数要处理数据的话它只能处理静态的数据。例子:
class account{
public:
staticdouble m_rate;
staticvoid set_rate(const double& x) {m_rate = x;}
};
double account::m_rate = 8.0;
int main(){
account::set_rate(5.0);
accounta;
a.set_rate(7.0);
}
静态的数据一般在类外面加定义,要不要给初值?都可以。
静态函数只能处理静态的数据,调用static函数的方式有两种,一种是通过object调用,第二种是通过class name来调用。
进一步补充:把ctors放在private区
Singleton
Class A{
Public:
StaticA& getInstance{return a;}
Setup(){…}
Private:
A();
A(const A&rhs);
Static A a;
}
A::getInstance().setup();
这个写法还不是最完美,虽然写好了a,但当外界都不需要用到,a仍然存在,更好的写法:
Meyers Singleton
class A{
public:
staticA& get Instance();
setup(){…}
private:
A();
A(constA& rhs);
…
};
A& A::getInstance(){
staticA a;
returna;
}
只有调用过getInstance后单例才开始存在。
进一步补充:cout
重载了各种数据的<<操作符
进一步补充:class template,类模板
template
把double都换成T,使用时
complex c1(2.5,1.5);
complex c2(2, 6);
模板可能会造成代码的膨胀,以上会产生两份相似的代码,但这个是必要的。
进一步补充:function template函数模板
stone r1(2, 3), r2(3, 3), r3;
r3 = min(r1, r2);
template
inline
const T& min(const T& a, constT& b){
returnb < a? b:a;
}
class stone{
public:
stone(intw, int h, int we)
:_w(w), _h(h), _weight(we) {}
booloperator < (const stone& rhs) const
{return_weight < rhs.weight;}
private:
int_w, _h, _weight;
};
进一步补充:namespace
namespace std{
…
}
using directive
#include
using namespace std;
int main(){
cin<< …;
cout<< …;
return0;
}
using declaration
using std::cout;
int main(){
std::cin<< …;
cout<< …;
return0;
}
#include
int main(){
std::cin<< …;
std::cout<< …;
return 0;
}
在这周的作业中我遇到了一个类公有继承了另一个类,并且在子类中含有一个指针指向一个类,在写这个类的拷贝构造函数时,需要将输入对象中指针下面的值赋给this对象中的指针,这里由于需要将输入对象leftup指针中的值取出,我在类point中写了两个取数据的公有方法叫getX和getY,对于赋值,我采用的方法是借用构造函数对新对象中的数据进行赋值。另一方面,对于类rectangle的父类中no的处理,我的理解是no用于记录每一个生成的shape的编号,这样就需要一个静态的全局数据count用于记录一共拥有的shape数,并在每生成一个新的shape时将对应的编号分给no,这个过程我在shape的构造函数中实现,对应的,在shape的析构函数中,我将count值减一,保证shape总数和现有的shape对象个数相同。在测试中,我先测试了拷贝构造和拷贝赋值的正常使用另外在删除其中一个对象后,再新生成一个rectangle,它对应的no应该是正确的。