(一)C++中的引用
1)引用就像能够自动的被编译器间接引用的常量型指针。它通常用于函数的参数表中和函数的返回值,但也可以独立使用。
使用引用的一些规则:
1.当引用被创建时,它必须被初始化
2.一旦一个引用被初始化为指向一个对象,它就不能改变为另一个对象的引用(指针则可以在任何时候指向另一个对象)
3.不可能有NULL引用。必须确保引用是一块合法的存储单元关联。
2)函数中的引用
最经常看见引用的地方实在函数参数和返回值中。当引用被用做函数参数时,在函数内任何堆引用的更改将对函数外的参数产生改变。如果从函数返回一个引用,必须像从函数返回一个指针一样对待。当函数返回时,无论引用关联的是什么都应该存在,否则,将不知到指向哪一个内存。
#include<iostream>
#include<stdio.h>
using namespace std;
int& h()
{
int a=10;
return a;
}
int main()
{
int b=h();
cout<<b<<endl;
}
上面这段代码虽然在我的电脑上能够执行,但是会返回一个警告,因为返回了一个局部变量的引用。但是如果将函数改为这样就可以了
int& h()
{
static int a=10;
return a;
}
把int a定义为一个静态变量,a便有了全局的生存空间,不再只是局部变量了。
常量引用
如果一个非常量引用函数传递一个常量对象的时候,这是不合法的。但是一个常量引用函数传递一个非常量对象的时候是合法的,这时候把这个传递的非常量看成一个常量,也就代表了,如果我再这个常量引用函数里面修改了这个传递进来的非常量,那么就会出错,如果不怎么明白请看下面的程序。
//常量引用传递非常量
#include<iostream>
#include<stdio.h>
using namespace std;
void g(const int& a)
{
// a++; 传递进来的是非常量,但是不允许修改这个非常量的值。
}
int main()
{
int a=10;
g(a);
cout<<a<<endl;
}
//非常量引用传递常量
//例如下面这段程序就是错误的
#include<iostream>
#include<stdio.h>
using namespace std;
void g(int& a)
{
// a++; 传递进来的是非常量,但是不允许修改这个非常量的值。
}
int main()
{
const int a=10;
g(a);
cout<<a<<endl;
}
上面的这种做法是比较安全的。所以如果知道这个函数不妨碍对象的不变性的化,让这个参数是一个常量引用将允许这个函数在任何情况下使用。这就意味着,对于内部类型,这个函数不会改变参数,而对于用户定义的类型,该函数只能够调用常量成员函数,而且不应当改变任何的公共成员函数。
在函数参数中使用常量引用特别重要。这是因为我们的函数也许会接受临时对象,这个临时对象是由两外一个函数的返回值创立,或者由函数使用者显式创立的。临时对象总是不变的,因此如果不使用常量引用,参数将不会被编译器接受。
请看下面这个例子:
#include<iostream>
#include<stdio.h>
using namespace std;
int f()
{
int a=1;
return a; //会返回一个临时对象,默认是常量
}
void g(const int& a) //如果这边不定义为常量引用,将无法接收临时对象
{
cout<<a<<endl;
}
int main()
{
g(1); //1也是一个临时对象
g(f());
}
上面这段代码,如果把函数常量引用改为函数非常量引用,便会出错了。
指针引用
在C语言中,如果想改变指针本身而不是它所指向的内容,函数可以如下声明:
void f(int**);
int i=47;
int *ip=&i;
f(&ip);
上面这段代码不得不说,是挺难看的,不怎么好理解,至少我当初学习C语言的时候就没怎么明白。
下面看C++如何来解决这个问题
//指针引用
#include<iostream>
#include<stdio.h>
using namespace std;
void increment(int*& i)
{
i++; //改变指针的指向,不会改变指针原来指向的内容
}
int main()
{
int a[10]={1,2,3};
int* i;
i=a;
cout<<"i= "<<i<<" "<<*i<<endl;
increment(i);
cout<<"i= "<<i<<" "<<*i<<endl;
}
//下面一段代码有错误,我也不知到哪儿错了,求高手指教
#include<iostream>
#include<stdio.h>
using namespace std;
void increment(int*& i)
{
i++;
}
int main()
{
int a[10]={1,2,3};
cout<<a<<" "<<*a<<endl;
increment(a);
cout<<a<<" "<<*a<<endl;
}
擦,上面的错误找到了,谢谢小聪子了,这个错误在他辅导我的时候教训了我很多次,但是我从没有记得过,嘿嘿。数组名不能用来当作左值,希望大家注意把~~~
参数传递准则
当给函数传递参数的时候,人么习惯上是通过常量引用来传递。
这种方法可以大大提高效率:传值方式需要调用构造函数和析构函数,然而如果不想改变参数,则可以通过常量引用传递,它仅需要将地址压栈。
事实上有一种情况不适合用传递地址方式,这就是当传值是唯一安全的途径,否则将会破坏对象时。想知道原因吗?接着往下看
(二)拷贝构造函数
位拷贝与初始化
看下面一段程序
#include<iostream>
#include<stdio.h>
using namespace std;
class HowMany
{
public:
HowMany();
static void print(const string& msg="");
~HowMany();
private:
static int objectCount;
};
int HowMany::objectCount=0;
HowMany::HowMany()
{
objectCount++;
}
void HowMany::print(const string& msg) //类里面默认实参只能在定义的时候
{
if(msg.size()!=0)
cout<<msg<<":";
cout<<"objectCount = "<<objectCount<<endl;
}
HowMany::~HowMany()
{
objectCount--;
print("~HowMany()");
}
HowMany f(HowMany x)
{
x.print("x argument inside f()");
return x;
}
int main()
{
HowMany h;
HowMany::print("after constructor of h");
HowMany h2=f(h);
HowMany::print("after call to f()");
}
运行结果:
after constructor of h:objectCount = 1
x argument inside f():objectCount = 1
~HowMany():objectCount = 0
after call to f():objectCount = 0
~HowMany():objectCount = -1
~HowMany():objectCount = -2
函数f的对象是通过按值传递的方式传入的对象的拷贝。然而,参数的传递是使用C的原始的位拷贝的概念,但C++ HowMany 类需要真正的初始化来维护它的完整性。所以,默认拷贝不能达到预期的效果。调用默认的拷贝构造函数并不会调用构造函数,默认呢拷贝构造函数仅仅按位复制,所以如果一个类里面有指针的时候,那么就会造成浅复制。
拷贝构造函数
当通过按值传递的方式传递一个对象的时候,就创立了一个新的对象,函数体内的对象是由函数体外的原来存在的对象传递的。从函数体返回对象也是同样的道理。
HowMany h2=f(h);
先前未创立的对象h2是由函数f()的返回值创建的,所以又从一个现有的对象中创建了一个新对象。
编译器假定我们想使用位拷贝来创建对象。在许多情况下,这是可行的。但是在HowMany类中就行不通,因为初始化不是简单的拷贝。正如上面所说,如果类中出现一个指针又将出现另一个问题,它们指向什么内容,是否拷贝它们或者它们是否与一些新的内存快相连。
不过还好,我们可以介入这个过程。并可以防止编译器进行位拷贝。每当编译器需要才能够现有的对象创建新对象时,可以通过定义自己的函数做这些事。因为是在创建新对象,所以,这个函数应该是构造函数,并且传递给这个函数的单一参数必须是创立对象的原对象。但是这个函数不能通过按值传递的方式传入构造函数,因为我们定义的这个构造函数正是为了处理按值传递的。这里引用就起作用了,可以使用源对象的引用,这个函数被称为拷贝构造函数。
如果设计了拷贝构造函数,当从现有的对象创建新对象时,编译器将不会使用位拷贝。编译器总是调用我们的拷贝构造函数。
所以上面程序稍作修改
HowMany(const HowMany& h); //类里面加上
HowMany::HowMany(const HowMany& h)
{
objectCount++;
}
为什么这样修改?默认复制构造函数是进行按位拷贝,一般情况下没什么错误,但是它仅对自己的成员变量拷贝,此处objectCount是一个静态变量,是属于一个类的,并不仅仅属于一个对象,所以在进行按位拷贝的时候,并不会对他进行操作,所以我们需要定义默认构造函数来对他进行操作。
思考:如果我们函数返回没有一个对象来接收,会发生什么事情,比如f(x)?
临时对象:首先我们得先明白返回对象生成的时间。当给函数传入一个对象的时候,调用复制构造函数,函数里会有一个拷贝对象,当函数需要返回的时候,会将这个拷贝对象再次拷贝一次,把拷贝的拷贝对象返回,而紧接着原来的拷贝对象删除。那么如果我们没有一个对象来接收这个拷贝的拷贝对象怎么办?系统会生成一个临时变量来存储。但是当这句话执行完后,这个临时变量就销毁了。
默认拷贝构造函数
因为拷贝构造函数实现按值传递方式的参数传递和返回,所以在这种简单结构情况下,编译器将有效地创建一个默认拷贝构造函数。知道目前所看到的一切默认的都是原始行为“位拷贝”。
当包括更复杂的类型时,如果没有创建拷贝构造函数,C++编译器也将自动的创建拷贝构造函数。
下面有个例子。设想创建一个类,它是由某些现有类的对象组成的。这个创建类的方法被称为组合,它是从现有类创建新类的方法之一。
#include<iostream>
#include<stdio.h>
#include<string>
using namespace std;
class WithCC
{
public:
WithCC(){}
WithCC(const WithCC&) //复制构造函数
{
cout<<"WithCC(WithCC&)"<<endl;
}
};
class WoCC
{
public:
WoCC(const string& ident=""):id(ident){} //构造函数
void print(const string& msg="") const
{
if(msg.size()!=0)
cout<<msg<<": ";
cout<<id<<endl;
}
private:
string id;
};
class Composite
{
public:
Composite():wocc("Composite()"){} //带有初始化列表的构造函数
void print(const string& msg="")const
{
wocc.print(msg);
}
private:
WithCC withcc;
WoCC wocc;
};
int main()
{
Composite c;
c.print("Contents of c");
cout<<"Calling Composite copy-constructor"<<endl;
Composite c2=c; //Composite无复制构造函数,复制构造函数自动被创建
c2.print("Contents of c2");
}
运行结果:
Contents of c: Composite()
Calling Composite copy-constructor
WithCC(WithCC&)
Contents of c2: Composite()
从上面这个例子来看,如果类是组合构成的,在没有定义拷贝构造函数的情况下,如果该对象原先的类有拷贝构造函数,那么就调用它自身的拷贝构造函数,如果没有则进行位复制。
替代拷贝构造函数的方法
有时候为了安全,我们直接不用拷贝构造函数,以防止出现一些错误
有一个简单的技术防止通过按值传递方式传递:声明一个私有拷贝构造函数。甚至不必去定义它,除非成员函数或友元函数需要执行按值传递方式的传递。如果用户试图用按值传递或返回对象,编译器将会发出一个出错信息。这是因为拷贝函数是私有的。因为已显示地声明我们接管了这项工作,所以编译器不再创建默认的拷贝构造函数。
#include<iostream>
#include<stdio.h>
#include<string>
using namespace std;
class NoCC
{
public:
Nocc(int ii):i(ii){}
private:
int i;
NoCC(const NoCC&);
};
void f(NoCC);
int main()
{
NoCC n;
f(n); //Error
NoCC n2=n; //Error
NoCC n3(n); //Error
}
上面这样就不能使用复制构造函数了。
(三)指向成员的指针
指针是指向一些内存的地址的变量,既可以是数据的地址也可以是函数的地址。所以,可以在运时改变指针指向的内容。C++的成员指针遵从同样的概念,除了所选择的内容是在类中之内的成员指针。这里麻烦的是所有指针需要地址,但是在类内部是没有地址的;选择一个类的成员意味着在类中偏移。只有把偏移和具体对象的开始地址相结合,才能得到实际地址。成员指针的语法要求选择一个对象的同时间接引用成员指针。
看了好一会儿,不理解,这掌的最后一部分先放这儿,慢慢理解。