引用
首先我们来看引用怎么使用,代码如下:
#include <iostream>
int main()
{
int val = 10;
int &refVal = val;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
return 0;
}
输出结果相信我们大家都很清楚了,没错,输出了两个10
我简单的修改一下代码,如下所示:
#include <iostream>
int main()
{
int val = 10;
int &refVal = val;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
val = 18;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
return 0;
}
此时的输出结果应该是怎么的呢?相信大家心里已经有了答案了。
那么,为什么会这样呢?我明明修改的是val的值呀,可为什么refVal的值也跟着变了呢?其实引用就相当于一个变量的别名。如果你不相信的话,请看下面的代码,这次改变我不再改变val的值,而是 改变refVal的值,看看结果会是怎样的呢?
#include <iostream>
int main()
{
int val = 10;
int &refVal = val;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
val = 18;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
refVal = 88;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
return 0;
}
还是直接公布答案吧。
相信大家看到这个结果之后,对引用是不是有了一个很深的理解了呢?
对!引用其实就是一个变量的别名!
那么既然说引用是一个变量的别名,那么它们的地址一样吗?大家可以先猜一下,然后再看输出结果,测试代码如下:
#include <iostream>
int main()
{
int val = 10;
int &refVal = val;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
std::cout << "val 地址:" << &val << std::endl;
std::cout << "refVal 地址:" << &refVal << std::endl;
return 0;
}
输出的地址居然相同,所以更加说明了引用就是变量的另一个别名。
到此, 引用的基本概念和作用相信大家已经明白了。
那么,引用和指针是什么关系呢?
指针的特性这里就不再举例说明了。
引用和指针的比较
引用是变量的别名,没有独立空间,和它所引用的变量使用的是同一个空间,它是强关联的关系(一旦确定,无法更改),对了,说到这里,忘记另一个例子,来说明这种强关联的特性,下面马上补上。
指针是另一种数据类型,它拥有独立的空间,它指向的是某一个变量,它是一种弱关联的关系。(指向的变量可以修改)。
下面补充引用的强关联性例子,代码如下:
#include <iostream>
int main()
{
int val = 10;
int &refVal = val;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
int a = 100;
refVal = a;
refVal = 30;
std::cout << val << std::endl;
std::cout << refVal << std::endl;
std::cout << a << std::endl;
return 0;
}
猜猜这个答案是什么?我们可以假设一下,或者反证一下,如果引用和指针一样,是一种弱关联的关系,那么此时的a因该是30了吧?但是事实上不是这样,a还是没有被改变。
所以,这个例子很好的说明了引用是一种弱关联。
引用的使用
既然引用和变量指向的是同一个地址,利用这样的特性,就经常来代替C语言的指针操作,比如我们最常用的交换函数,如果在C语言中,必须使用指针才能达到目的,在C++中,我们就可以使用引用达到此目的了。请看下面的例子:
#include <iostream>
void Swap(int lhs, int rhs)
{
int temp = lhs;
lhs = rhs;
rhs = temp;
}
int main()
{
int a = 10;
int b = 20;
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
Swap(a, b);
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
return 0;
}
这样肯定是交换不成功的,相信大家已经知道为什么交换不成功了,这里就不再解释为什么交换失败了。
我们对代码稍做修改:
#include <iostream>
void Swap(int &lhs, int &rhs)
{
int temp = lhs;
lhs = rhs;
rhs = temp;
}
int main()
{
int a = 10;
int b = 20;
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
Swap(a, b);
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
return 0;
}
没错,就简单的修改了一交换函数的一个引用,其它的使用方式都不变,我们再看一下结果
是不是很神奇?我们没有使用指针,但是达到了使用指针的效果,从这上面,我们是不是也看到了引用的另一个特点呢?也许你早就想到了,那就是传递效率高,它相当于传递的变量的地址,所以省去了传参时的临时变量的生成,特别是对于占用空间很大的结构体,使用引用效率就更高了。
const引用
有时候我们想用引用,但又不想让调用者有意或者无意的修改我们的变量,那么我们可以使用常引用,这样既达到了参数的传递效率,又保证了参数的安全性,更方便了调用者的使用和理解,真所谓一举多得呀。
请看下面的代码:
#include <iostream>
void Swap(const int &lhs, const int &rhs)
{
int temp = lhs;
lhs = rhs;
rhs = temp;
}
int main()
{
int a = 10;
int b = 20;
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
Swap(a, b);
std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
return 0;
}
上面的代码是不是有问题呢?答案是可定的,编译都不会通过,
它会提示无法给常量赋值,是不是保证了我们变量的安全性呢?
那么,问题又来了,引用这么好用,那干脆在C++中都使用引用得了,再也不用使用招人讨厌的指针了。不,我们还不能完全丢弃指针,引用指针毕竟是指针,还是有着不可替代的地位。在堆上分配的空间就不可以使用引用,我们先记住这个特点吧,例子以后有时间再举吧。
对了,引用还有另一个作用,就是可以作为函数的返回值,这一点在类中体现的会更好,但是我们还是在这里说了吧。
请看下面的代码:
#include <iostream>
int g_array[10] = { 1, 3, 5,7,9,1,3,65, };
int &GetValByIndex(int index)
{
return g_array[index];
}
int main()
{
std::cout << "g_array[2]: " << g_array[2] << std::endl;
GetValByIndex(2) = 100;
std::cout << "g_array[2]: " << g_array[2] << std::endl;
return 0;
}
上面的两条打印结果一样吗?可定不一样的了。
类
类是C++中的一个十分重要的特性,这也是它的强大之所在,有了类,我们的软件才能进行面向对象或者基于对象的进行编程,使得软件的可扩展行增强。
#include <iostream>
class Person
{
int age = 20;
};
int main()
{
Person p;
p.age = 21;
return 0;
}
首先,这个代码有问题吗?如果不太清楚,那就直接看答案吧,我们看是否能够编译通过
很抱歉,编译没有通过,仔细一看提示,原来我们访问的是类中的私有成员,而私有成员是不允许外部访问的,所以我们还需要更改这个类成员的权限,把它改为公有的。
#include <iostream>
class Person
{
public:
int age = 20;
};
int main()
{
Person p;
p.age = 21;
return 0;
}
编译顺利的通过,我们从上面的代码中,也了解到了类的一个特点,默认情况下的权限是私有的。
但是我们一般不会这样使用类,不会直接访问类中的属性(age就是这个类的属性),而是通过类提供的方法来供外部使用。所以代码还需要稍作修改:
#include <iostream>
class Person
{
public:
void SetAge(int age)
{
m_age = age;
}
int GetAge()
{
return m_age;
}
private:
int m_age;
};
int main()
{
Person p;
std::cout << p.GetAge() << std::endl;
p.SetAge(23);
std::cout << p.GetAge() << std::endl;
return 0;
}
为了区别于普通的变量,我们类的成员变量都是以m_开头的,也有其它的命名方式,这里我们就不多做介绍了。
这里为什么第一个数打印随机数了呢?原因很简单,因为我们没有初始化,那怎么初始化呢?这就是我们接下来要讲的类的构造函数以及析构函数了。
如果我们不显示写类的构造和析构函数,那么编译器会自动的给我们生成一个默认的空的构造和析构函数。
有了类的构造函数,那么我们初始化变量就方便多了。
#include <iostream>
class Person
{
public:
Person(){}
~Person(){}
public:
void SetAge(int age){ m_age = age; }
int GetAge(){return m_age;}
private:
int m_age;
};
int main()
{
Person p;
std::cout << p.GetAge() << std::endl;
p.SetAge(23);
std::cout << p.GetAge() << std::endl;
return 0;
}
这里的构造和析构函数和编译器默认提供的构造和析构函数是等价的,都是什么也没有做。此时我们可以在我们的构造函数中做一些处理,比如,初始化变量:
#include <iostream>
class Person
{
public:
Person() { m_age = 20; }
~Person(){}
public:
void SetAge(int age){ m_age = age; }
int GetAge(){return m_age;}
private:
int m_age;
};
int main()
{
Person p;
std::cout << p.GetAge() << std::endl;
p.SetAge(23);
std::cout << p.GetAge() << std::endl;
return 0;
}
此时打印出来的还是随机数码?我们看一下结果便可知道。
从打印结果可以看出,我们在默认函数中做的操作生效了。
前面我们说过C++支持函数的重载,那么构造函数可以被重载吗?如果不知道,我们也不要猜了,试试不就知道了嘛!
#include <iostream>
class Person
{
public:
Person(int age) { m_age = age; }
~Person(){}
public:
void SetAge(int age){ m_age = age; }
int GetAge(){return m_age;}
private:
int m_age;
};
int main()
{
Person p;
std::cout << p.GetAge() << std::endl;
p.SetAge(23);
std::cout << p.GetAge() << std::endl;
return 0;
}
这样是不是很完美了?我们可以在构造函数中给参数传递参数了。但是在编译的时候,我们就郁闷了,你会发现编译不通过。
会提示我们没有合适的默认构造函数,这是为啥呢?因为当我们自己实现构造函数时,编译器就不再为我们提供默认的构造函数了,此时我们要适当的修改我们的代码,才能顺利通过编译。有两种修改方式,一种是修改使用类的方式,代码如下:
#include <iostream>
class Person
{
public:
Person(int age) { m_age = age; }
~Person(){}
public:
void SetAge(int age){ m_age = age; }
int GetAge(){return m_age;}
private:
int m_age;
};
int main()
{
Person p(18);
std::cout << p.GetAge() << std::endl;
p.SetAge(23);
std::cout << p.GetAge() << std::endl;
return 0;
}
编译顺利通过,那么运行正常吗?肯定没问题了,这里就不再显示运行结果了。
还有另一种修改方式,那就是再给我们的类加上一个无参数的构造函数,代码如下:
#include <iostream>
class Person
{
public:
Person() { m_age = 15; }
Person(int age) { m_age = age; }
~Person(){}
public:
void SetAge(int age){ m_age = age; }
int GetAge(){return m_age;}
private:
int m_age;
};
int main()
{
Person p;
std::cout << p.GetAge() << std::endl;
p.SetAge(23);
std::cout << p.GetAge() << std::endl;
return 0;
}
此时的编译也是没有问题的,运行也是ok的。
以上只是讲了类的构造函数,类不是还有一个析构函数吗?我们怎么一直没有说怎么使用呢?是不是没有什么用呢?当然不是没有用啦!下面我们就介绍一下类的析构函数。
类的析构函数的主要作用是对象销毁时的一些收尾工作。上面的例子不适合做析构函数的演示,我们再举一个别的例子。
#include <iostream>
#include <cstring>
class MyString
{
public:
void SetString(char *str)
{
m_pBuf = str;
}
private:
char *m_pBuf;
};
int main()
{
return 0;
}
这样是不是很完美,如果你也认为是这样的话,那就大错特错了。上面的代码犯了两点错误:
第一:字符串不能直接使用“=”进行赋值,这样只是进行了地址的赋值;
第二:如果使用strcpy,那么我们还需要给m_pBuf分配空间;
那么该如何分配空间呢?对了,我们有构造函数啊,我们可以在构造函数中把空间给分配了。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
class MyString
{
public:
MyString() { m_pBuf = new char[100]; }
void SetString(char *str)
{
strcpy(m_pBuf, str);
}
private:
char *m_pBuf;
};
int main()
{
return 0;
}
这样是不是就完美了呢?错,我们还有一步没做,那就是new出来的空间在哪儿释放呢?我们还可以指定分配空间的大小。就不演示了。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
class MyString
{
public:
MyString() { m_pBuf = new char[100]; }
~MyString() { delete[] m_pBuf; }
void SetString(char *str)
{
strcpy(m_pBuf, str);
}
char *GetString() { return m_pBuf; }
private:
char *m_pBuf;
};
int main()
{
MyString mystr;
mystr.SetString("Hello World!");
std::cout << mystr.GetString() << std::endl;
return 0;
}
输出结果是:
这样在对象销毁时,就保证了这个对象占用的空间被系统回收了,避免了内存的泄露。