重要的经验
1)在设计函数时,参数若为类类型,则一般都用引用类型,如果为内部数据结构则不适用引用。
2)在写if语句的时候注意应该把常量放在变量之前,防止一下错误 if(a=1){...}编译器不会报错,而如果写成if(1=a){...}
编译器则会报错。这是因为a=1此时if条件判断a为非零,此时成功通过编译。而1=a则为非法赋值,编译报错。此外,如果a=0,if
条件检测a为零值,条件为假,不执行。
c++语法点
面向对象的三个基本特征:封装,继承,多态。
运算符重载
1)增量操作符
前增操作符使用X operator++(X& a),而后增操作符使用X operator++(X& a, int)以示区别。
此外在定义后增操作符时候应该在函数中定义一个零时变量用于放回赋值运算符和复制构造函数都是用已存在的B对象来创建另一个对象A。
2)赋值运算符与拷贝构造函数的区别
不同之处在于:
赋值运算符处理两个已有对象,即赋值前B应该是存在的;
复制构造函数是生成一个全新的对象,即调用复制构造函数之前A不存在。
CTemp a(b); //复制构造函数,C++风格的初始化
CTemp a=b; //仍然是复制构造函数,不过这种风格只是为了与C兼容,与上面的效果一样,在这之前a不存在,或者说还未构造好。
CTemp a;
a=b; //赋值运算符
在这之前a已经通过默认构造函数构造完成。
实例总结:
重点:包含动态分配成员的类 应提供拷贝构造函数,并重载"="赋值操作符。
以下讨论中将用到的例子:
class CExample
{
public:
CExample(){pBuffer=NULL; nSize=0;}
~CExample(){delete pBuffer;}
void Init(int n){ pBuffer=new char[n]; nSize=n;}
private:
char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
int nSize;
};
这个类的主要特点是包含指向其他资源的指针。
pBuffer指向堆中分配的一段内存空间。
一、拷贝构造函数
调用拷贝构造函数1
int main(int argc, char* argv[])
{
CExample theObjone;
theObjone.Init(40);
//现在需要另一个对象,需要将他初始化称对象一的状态
CExample theObjtwo=theObjone;//拷贝构造函数
...
}
语句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
其完成方式是内存拷贝,复制所有成员的值。
完成后,theObjtwo.pBuffer==theObjone.pBuffer。
即它们将指向同样的地方(地址空间),指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。
所以需要采用必要的手段来避免此类情况。
回顾以下此语句的具体过程:通过拷贝构造函数(系统默认的)创建新对象theObjtwo,并没有调用theObjtwo的构造函数(vs2005试验过)。
可以在自定义的拷贝构造函数中添加输出的语句测试。
注意:对于含有在自由空间分配的成员时,要使用深度复制,不应使用浅复制。
调用拷贝构造函数2
当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数。
例如
BOOL testfunc(CExample obj);
testfunc(theObjone); //对象直接作为参数。
BOOL testfunc(CExample obj)
{
//针对obj的操作实际上是针对复制后的临时拷贝进行的
}
调用拷贝构造函数3
当函数中的局部对象被被返回给函数调者时,也将建立此局部对象的一个临时拷贝,拷贝构造函数也将被调用
CTest func()
{
CTest theTest;
return theTest
}
二、赋值符的重载
下面的代码与上例相似
int main(int argc, char* argv[])
{
CExample theObjone;
theObjone.Init(40);
CExample theObjthree;
theObjthree.Init(60);
//现在需要一个对象赋值操作,被赋值对象的原内容被清除,
//并用右边对象的内容填充。
theObjthree=theObjone;
return 0;
}
也用到了"="号,但与(一)中的例子并不同,(一)的例子中,"="在对象声明语句中,表示初始化。更多时候,这种初始化也可用括号表示。例如 CExample theObjone(theObjtwo);
而本例子中,"="表示赋值操作。将对象theObjone的内容复制到对象theObjthree;,这其中涉及到对象theObjthree原有内容的丢弃,新内容的复制。
但"="的缺省操作只是将成员变量的值相应复制。旧的值被自然丢弃。
由于对象内包含指针,将造成不良后果:为了避免内存泄露,指针成员将释放指针所指向的空间,以便接受新的指针值,这正是由赋值运算符的特征所决定的。但如果是"x=x"即自己给自己赋值,会出现什么情况呢?x将释放分配给自己的内存,然后,从赋值运算符右边指向的内存中复制值时,发现值不见了。
因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载"="赋值操作符号。
类定义变为:
class CExample
{
...
CExample(const CExample&); //拷贝构造函数
CExample& operator = (const CExample&); //赋值符重载
...
};
//赋值操作符重载
CExample & CExample::operator = (const CExample& RightSides)
{
nSize=RightSides.nSize; //复制常规成员
char *temp=new char[nSize]; //复制指针指向的内容
memcpy(temp, RightSides.pBuffer, nSize*sizeof(char));
delete []pBuffer;
//删除原指针指向内容(将删除操作放在后面,避免X=X特殊情况下,内容的丢失)
pBuffer=temp; //建立新指向
return *this
}
三、拷贝构造函数使用赋值运算符重载的代码。
CExample::CExample(const CExample& RightSides)
{
pBuffer=NULL;
*this=RightSides //调用重载后的"="
}
class String
{
public:
String(const char*s=0);
String(const String &);
~String();
String &operator=(const String &);
// String& operator=(const char *);
String& operator+=(const String &);
// String& operator+=(const char *);
String operator+(const String&);
// String operator+(const char *);
char& operator[](int) const;
// bool operator==(const char *) const;
bool operator==(const String&) const;
int size() { return Length; }
char *Point() { return Str; }
private:
int Length;
char *Str;
};
String(const char*s=0);//转换构造函数
转换构造函数可以将char *字符串转换为String对象,当系统进行隐式转换时,C++会隐式的自动调用此转换函数。因此,在类中没有必要再对char * 类型进行运算符重载。
String A;
Char * p;
A+p;//首先调用转化构造函数,然后调用String operator+(const String&);
String(const String &);//拷贝构造函数
String &operator=(const String &);//赋值运算符重载
拷贝构造函数用于复制一个String类,产生一个新类,新的类和原来的类数据成员完全相同;赋值运算符重载函数用于把一个类赋值给另一个类,是这两个类相等。
注意:拷贝构造函数参数必须是一个引用,如果不是一个引用,而是按值传递的话,当传递值得时候,需要对原对象进行复制,就会调用拷贝构造函数,这样会是一个递归调用,而且是一个死循环的调用。
他们的区别是拷贝构造函数用于对一个新的对象初始化,而赋值运算符重载用于对一个已经构造好初始化完成甚至使用过的对象进行修改值。赋值操作符重载比拷贝构造函数做得要多,它除了完成拷贝构造函数所完成的拷贝动态申请的内存的数据之外,还释放了被赋值对象申请的内存空间。
String a(“asdf”);
String b(a);//拷贝构造函数
String b=a;//注意,这行代码并不是赋值运算符重载,而是拷贝构造函数,他和上一行代码所实现的功能完全相同。
String b(“sssss0”);
b=a;//这个才是赋值运算符重载,他对已初始化的对象进行重新赋值。
C++有一个默认的拷贝构造函数和运算符重载函数,当没有显式的定义时,编译器当需要的时候隐式调用,将所有对应的数据成员进行复制。但是,当类中含有一个指向一块内存区域的指针,比如char *p;这样会使两个类指向同一块内存区域,当一个对象析构后,另一个对象的指针变成为空指针。所以,对于涉及到动态内存分配的类,要显式的定义拷贝构造函数,运算符重载,还有析构函数。
3)const与define的区别
define部分:
宏不仅可以用来代替常数值,还可以用来代替表达式,甚至是代码段。(宏的功能很强大,但也容易出错,所以其利弊大小颇有争议。)
宏的语法为:
#define 宏名称 宏值
注意,宏定义不是C或C++严格意义上的语句,所以其行末不用加分号结束。
作为一种建议和一种广大程序员共同的习惯,宏名称经常使用全部大写的字母。
利用宏的优点:
1)让代码更简洁明了。当然,这有赖于你为宏取一个适当的名字。一般来说,宏的名字更要注重有明确直观的意义,有时宁可让它长点。
2)方便代码维护
对宏的处理,在编译过程中称为“预处理”。也就是说在正式编译前,编译器必须先将代码出现的宏,用其相应的宏值替换,这个过程有点你我在文字处理软件中的查找替换。所以在代码中使用宏表达常数,归根结底还是使用了立即数,并没有明确指定这个量的类型。这容易带来一些问题,所以C++使用另一更稳妥的方法来代替宏的这一功能。
const部分
常量定义的格式为:
const 数据类型 常量名 = 常量值;
而const定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序可能出现错误进行排查。常量必须一开始就指定一个值,然后,在以后的代码中,我们不允许改变此常量的值。
两者之间的区别:
内存空间的分配上。define进行宏定义的时候,不会分配内存空间,编译时会在main函数里进行替换,只是单纯的替换,不会进行任何检查,比如类型,语句结构等,即宏定义常量只是纯粹的置放关系,如#define null 0;编译器在遇到null时总是用0代替null它没有数据类型(还有疑问请找C语言书籍看预处理部分或者看MSDN.而const定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序可能出现错误进行排查,所以const与define之间的区别在于const定义常量排除了程序之间的不安全性.
4)使用参数列表与主体函数初始化对象的区别
C++ Primer中在讲构造函数初始化列表的时候有这么一段话:
无论是在构造函数初始化列表中初始化成员,还是在构造函数体中对它们赋值,最终结果是相同的。
但是还是有区别的:
首先把数据成员按类型分类
1。内置数据类型,复合类型(指针,引用)
2。用户定义类型(类类型)
分情况说明:
对于类型1,在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
对于类型2,结果上相同,但是性能上存在很大的差别
因为类类型的数据成员对象在进入函数体是已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,这是调用一个构造函数,在进入函数体之后,进行的是 对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
举个例说明
class A;
class B
{
public:
B(){a = 3;}
private:
A a;
}
class A
{
public:
A(){}
A(int){value = 3;}
int value;
}
像上面,我们使a对象的value为3,调用一个A的构造函数一个默认拷贝赋值符,才达到目的
B::B():a(3){}
像这样,只调用了一个构造函数就达到了所需的对象啦,所以性能好的
我的问题是关于初始化C++类成员的。我见过许多这样的代码(包括在你的栏目中也见到过):
CSomeClass::CSomeClass(){ x=0; y=1; }
而在别的什么地方则写成下面的样子:
CSomeClass::CSomeClass() : x(0), y(1){……}
我的一些程序员朋友说第二种方法比较好,但他们都不知道为什么是这样。你能告诉我这两种类成员初始化方法的区别吗?
从技术上说,你的程序员朋友是对的,但是在大多数情况下,两者实际上没有区别。有两个原因使得我们选择第二种语法,它被称为成员初始化列表:一个原因是必须的,另一个只是出于效率考虑。
让我们先看一下第一个原因——必要性。设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。
class CMember {
public:
CMember(int x) { ... }
};
因为Cmember有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建Cmember的一个实例。
CMember* pm = new CMember; // Error!!
CMember* pm = new CMember(2); // OK
如果Cmember是另一个类的成员,你怎样初始化它呢?你必须使用成员初始化列表。
class CMyClass {
CMember m_member;
public:
CMyClass();
};
//必须使用成员初始化列表
CMyClass::CMyClass() : m_member(2){ ……}
没有其它办法将参数传递给m_member,如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。
第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时。MFC的Cstring提供了一个完美的例子。假定你有一个类CmyClass具有一个Cstring类型的成员m_str,你想把它初始化为"yada yada."。你有两种选择:
CMyClass::CMyClass()
{
// 使用赋值操作符
// CString::operator=(LPCTSTR);
m_str = _T("yada yada");
}
//使用类成员列表
// and constructor CString::CString(LPCTSTR)
CMyClass::CMyClass() : m_str(_T("yada yada"))
{
}
在它们之间有什么不同吗?是的。编译器总是确保所有成员对象在构造函数体执行之前初始化,因此在第一个例子中编译的代码将调用CString:: Cstring来初始化m_str,这在控制到达赋值语句前完成。在第二个例子中编译器产生一个对CString:: CString(LPCTSTR)的调用并将"yada yada"传递给这个函数。结果是在第一个例子中调用了两个Cstring函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。在 Cstring的例子里这是无所谓的,因为缺省构造函数是内联的,Cstring只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言, 重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。在内部类型如ints或者longs或者其它没有构 造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始 化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。
在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和 添加注释,你可以写出这样的语句:x=y=z=0;或者memset(this,0,sizeof(this));
注意第二个片断绝对是非面向对象的。
当我考虑初始化列表的问题时,有一个奇怪的特性我应该警告你,它是关于C++初始化类成员的,它们是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
class CMyClass
{
CMyClass(int x, int y);
int m_x;
int m_y;
};
CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)
{
}
你 可能以为上面的代码将会首先做m_y=I,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺 序声明的。结果是m_x将有一个不可预测的值。我的例子设计来说明这一点,然而这种bug会更加自然的出现。有两种方法避免它,一个是总是按照你希望它们 被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
注意:
通过参数列表及默认构造函数我们可以写出:
class complex
{
double re,im;
public:
complex(double r = 0, double i = 0): re(r), im(i){}
...
}
代替:
class complex
{
double re,im;
pubblic:
complex():re(0),im(0){}
complex(double r): re(r),im(0){}
complex(double r,double i): re(r),im(i){}
...
}
5)解决自定义操作符缺少自反性的问题
可以采用参数不同组合式,但是这种方法存在组合爆炸问题。因此可以采用另一种方法,就是利用转换构造函数,将其他类型转换为类类型。如下例子所示:
class complex
{
public:
double re,im;
complex(int a):re(a),im(0){}
complex(double r = 0, double i = 0): re(r),im(i){}
/* 在对象中采用此方法不通 ,这是因为只用对象才能调用该操作符,如果1+a由于1 不是complex对象因此编译出错。
complex operator+(complex a)
{
re += a.re;
im += a.im;
return *this;
}*/
};
//在可以采用如下方法。
//complex operator+( complex a, complex b )
//{
// complex temp;
// temp.re = a.re + b.re;
// temp.im = a.im + b.im;
// return temp;
//}
int main()
{
complex a = complex(1,1);
complex b = complex(2,2);
a = a+1;
cout<
c++语法点
最新推荐文章于 2024-08-05 12:39:56 发布