一些小知识点
关于C和C++
在C语言中,数据和方法(函数)是分开的,用数据来创造变量。
而在C++的类中(struct,class),数据和函数是可以放在一起的(成员数据,成员函数),由这两者共同产生对象。
关于class,也可以看作两大类:带有指针的和不带有指针的。
这两类在实现上有一定的区别,因为不带有指针的可以被class包含在一起。
而带有指针的(比如字符串),会在内存其他地方调用空间,在类中只有指向该空间的指针。
关于头文件
#ifndef ...
#define ...
...
#endif
这样的声明属于防卫式声明,可以防止多个文件中重复的include。
关于内联函数(inline)
内联函数可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。
这里提到一个inline候选人的概念,也就是说是不是真正的inline需要由编译器来决定,函数越简单越有可能。
如果函数在class内定义,则自动成为inline候选人。
如果在class外,可以在前面加上inline声明,但是能不能被选入要看编译器。
inline double imag(const complex& x){
return x.imag();
}
关于初始化列表
有时的构造函数会以初始化列表的形式出现。
class complex{
public:
complex (double r=0,double i=0)
:re(r),im(i){}
private:
double re,im;
}
也可以使用赋值的方法,比如re=r,但在效率上有差异,这也是使用初始化列表的原因。
这里的例子使用了默认实参,要注意默认实参重载时可能会与无参数发生冲突,编译器无法判断属于哪一类。
构造函数也可以放在private区,也就是不允许外界构造。(是一种设计模式Singleton)
关于const
后面会对const深入探讨,这里先引入一下。我们可以换个角度理解const。在你不想改变值的情况下就加上const,可以在函数后或者变量之前。
class complex{
public:
complex (double r=0,double i=0) :re(r),im(i){}
double real() const {return re;}
double imag() const {return im;}
private:
double re,im;
}
但如果不加,在下面这种情况就会报错
const complex c1(2,1);
cout<<c1,real();
cout<<c1.imag();
编译器会认为这与类的构建产生了冲突。
比较重要的知识点
关于参数传递
参数传递有传值和传指针、引用。
在我们第一次遇见传值和传指针传引用时,应该是遇到swap的时候。
void swap(int a,int b)
{
int temp = a;
a = b;
b = temp;
}
int main(void)
{
int a = 10;
int b = 20;
printf("before swap:a = %d,b = %d\n",a,b);
swap(a,b);
printf("after swap:a = %d,b = %d\n",a,b);
return 0;
这时候的a与b没有被交换,因为swap函数传入的是值。
传值就相当于创建了原始数据的副本。副本如何操作与原始数据本身没有关系。
而传地址和传指针会改变数据本身,才会实现真正的交换。
(关于引用和指针的关系后面细说,可以把引用看作包装后的指针)
能选择传递引用就不用传递值,引用速度更快(相当于传指针),可以加const让函数不可修改。
ostream&
operator<<(ostream& os,const complex& x){
return os<<'('<<real(x)<<','<<imag(x)<<')';
}
关于this
this可以看作这个class里该对象的地址。
成员函数里会隐含一个this在里面。
inline complex& complex::operator+=(const complex& r){
return _doapl(this,r);
}
inline complex& _doapl(complex* ths,const complex& r){
this->re+=r.re;
this->im+=r.im;
return *ths;
}
这时如果执行c2+=c1;c2就相当于this,c1就是r。(注意操作符重载作用于左边)
inline complex& complex::operator+=(this,const complex& r){
return _doapl(this,r);
}
非成员函数无this,比如
inline complex operator+(const complex& x,const complex& y){
return ...
}
关于返回值传递
返回值传递也分为传值和传参。
区别在于:
- 如果返回的值需要一块新的空间来存放,那么就需要传递值,因为函数结束后不保存就会消失。
- 如果是在已经存在的值上面叠加等操作,不需要新的空间,那么可以回传引用。
根据上面的例子分开来说明这两种情况。
首先是成员函数
inline complex& complex::operator+=(const complex& r){
return _doapl(this,r);
}
inline complex& _doapl(complex* ths,const complex& r){
this->re+=r.re;
this->im+=r.im;
return *ths;
}
可以看到这里的操作是在已存在的this上进行的,所以可以返回引用。
传递者无需知道接收者的接收形式,这也是引用的好处
比如_doapl这个函数返回的是*ths,也就是value值本身,但是接收者用引用接收。试想一下如果这里是指针接收那么就需要返回指针,但是引用不需要。
而且返回引用可以实现连续操作。比如c1+=c2+=c3;如果传值那么就会出现上述的不发生改变的情况。
对于非成员函数的例子来说
inline complex operator+(const complex& x,const complex& y){
return complex(real(x)+real(y),imag(x)+imag(y));
}
注意他们的返回值是由临时对象构建的,也就是说需要新的空间存放,否则会跟着函数消亡。
所以这时返回需要传递值。
关于三大函数
在类中有三个比较特殊的函数:拷贝构造、拷贝赋值、析构
接下来以字符串类为例阐述。
class String
{
public:
String(const char* cstr=0);//构造函数
String(const String& str);//拷贝构造
String& operator=(const String& str);//拷贝赋值
~String();//析构
char* get_c_str() const {return m_data;}
private:
char* m_data;
}
字符串类的特点是它是由指针构成的。而指针指向的空间才真正存放字符串。
对于c++来说,这个字符串结尾为’\0’,占一位。
构造函数
String类的构造函数和析构函数如下,写的很精致。
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;
}
我们根据以下调用来看
String s1();
String s2("hello");
String的构造函数有默认参数为0,如果不为零,则需要分配一块新的空间,大小为该字符串+1(需要结尾的’\0’),然后将字符串拷贝。
如果为0,也就是没有指定初值,那么需要new一块大小为1的空间存放’\0’。
拷贝构造
对于拷贝构造来说,需要给新字符串开辟新空间,再复制原字符串
inline String::String(const String& str)
{
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
}
String s1("hello");
String s2(s1);
拷贝赋值
拷贝赋值是我们常见的"=“形式,虽然是重载”=",但实际实现也有一些小细节
inline String& String::operator=(const String& 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 s2=s1;
会首先把s2的原值delete,然后开辟新空间,再将s1的值复制进去。
这里第一步先进行了检测,检测这个要赋的值是不是自己。
这里不是简单地提升效率,如果我们注意到后面的delete就会发现,当等于号左右都是自己的时候,会出现销毁自己的情况,这样就无法完成赋值了。
关于浅拷贝与深拷贝
在我们明白上述几种函数怎么写的时候,浅拷贝和深拷贝的问题就迎刃而解了。
如果我们不写String类的特殊构造函数,让编译器负责处理。
那么在拷贝构造与拷贝赋值时,就会出现直接拷贝的情况,没有new和delete的过程。
因为String是指针类,那么这样就会导致两个指针指向同一块空间,造成内存泄漏。这就是浅拷贝。
深拷贝就是我们这种正确的写法,将每个指针对应于一块内存空间。
所以结论就是带有指针的类必须要有拷贝构造和拷贝赋值,不然会出现浅拷贝现象