1.引用
1.引用基本用法
引用是c++对c的重要扩充。在c/c++中指针的作用基本都是一样的,但是c++增加了另外一种给函数传递地址的途径,这就是按引用传递(pass-by-reference),它也存在于其他一些编程语言中,并不是c++的发明。
■变量名实质上是一段连续内存空间的别名,是一个标号(门牌号)
■程序中通过变量来申请并命名内存空间
■通过变量的名字可以使用存储空间
对一段连续的内存空间只能取一个别名吗?
c++中新增了引用的概念,引用可以作为一个已定义变量的别名。
基本语法:
Type &ref = val;
注意事项:
■&在此不是求地址运算,而是起标识作用。
■类型标识符是指目标变量的类型
■必须在声明引用变量时进行初始化。
■引用初始化之后不能改变。
■不能有NULL引用。必须确保引用是和一块合法的存储单元关联。
■ 建立对数组的引用。
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
void test01();
void test02();
int main()
{
//test01();
system("pause");
return EXIT_SUCCESS;
}
//1. 认识引用
void test01()
{
int a = 10;
//给变量a取一个别名b
int & b = a;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "------------" << endl;
//操作b就相当于操作a本身
b = 100;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "------------" << endl;
//一个变量可以有n个别名
int & c = a;
c = 200;
cout << "a:" << a << endl;
cout << "b:" << b << endl;
cout << "c:" << c << endl;
cout << "------------" << endl;
//a,b,c的地址都是相同的
cout << "a:" << &a << endl;
cout << "b:" << &b << endl;
cout << "c:" << &c << endl;
}
//2. 使用引用注意事项
void test02()
{
//1) 引用必须初始化
//int & ref; //报错:必须初始化引用
//2) 引用一旦初始化,不能改变引用
int a = 10;
int b = 20;
int & ref = a;
ref = b; //不能改变引用
}
调用test01()函数输出结果:
a:10
b:10
------------
a:100
b:100
------------
a:200
b:200
c:200
------------
a:0000008C24F4FA64
b:0000008C24F4FA64
c:0000008C24F4FA64
请按任意键继续. . .
调用test02()函数输出结果:
a:20
b:20
------------
a的地址:00000060C60FF404
b的地址:00000060C60FF424
请按任意键继续. . .
2.建立数组引用:
int arr[] = { 1, 2, 3, 4, 5 };
//第一种方法
//1.定义数组类型
typedef int(MY_ARR)[5];//数组类型
//2.建立引用
MY_ARR &arref = arr;//建立引用,int &b=a;
//第二种方法
//直接定义引用
int(&arref2)[5] = arr;// int &b=a
//第三种方法
typedef int(&MY_ARR3)[5];//建立引用数组类型
MY_ARR3 arref3 = arr;
3.引用的本质
引用的本质在c++内部实现是一个常指针.
Type &ref = val; // Type * const ref = &val;
c++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同,只是这个过程是编译器内部实现,用户不可见。
//发现是引用,转换为 int* const ref = &a;
void testFunc(int& ref){
ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
int a = 10;
int &aRef = a; //自动转换为int * const aRef = &a;这也能说明引用为什么必须初始化
aRef = 20; //内部发现aRef是引用,自动帮我们转换为: *aRef = 20;
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
testFunc(a);
return EXIT_SUCCESS;
}
4. 指针引用
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
void test01()
{
char* p = "翠花";
char* &p1 = p;
cout << p1 << endl;
}
//被调函数
void func(char* &tmp)
{
char *p;
p=(char*)malloc(64);
memset(p, 0, 64);
strcpy(p, "小花");
tmp = p;//省了*
}
//主调函数
void test02()
{
char * mp = NULL;
func(mp);//省了&
cout << mp << endl;
}
int main()
{
test02();
system("pause");
return EXIT_SUCCESS;
}
在c语言中如果想改变一个指针的指向而不是它所指向的内容,函数声明可能这样:
void fun(int**);
给指针变量取一个别名。
Type * pointer = NULL;
Type * &p = pointer;
struct Teacher
{
int mAge;
};
//指针间接修改teacher的年龄
void AllocateAndInitByPointer(Teacher** teacher)
{
*teacher = (Teacher*)malloc(sizeof(Teacher));
(*teacher)->mAge = 200;
}
//引用修改teacher年龄
void AllocateAndInitByReference(Teacher*& teacher)
{
teacher->mAge = 300;
}
void test()
{
//创建Teacher
Teacher * teacher = NULL;
//指针间接赋值
AllocateAndInitByPointer(&teacher);
cout << "AllocateAndInitByPointer:" << teacher->mAge << endl;
//引用赋值,将teacher本身传到ChangeAgeByReference函数中
AllocateAndInitByReference(teacher);
cout << "AllocateAndInitByReference:" << teacher->mAge << endl;
free(teacher);
}
5.常量的引用
常量引用的定义格式:
const Type& ref = val;
常量引用注意:
■字面量不能赋给引用,但是可以赋给const引用
■ const修饰的引用,不能修改。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int main02()
{
//普通引用
int a = 10;
int &ref = a;
ref = 20;
//int &ret2 = 10;//不能给字面量取别名 err
const int &ref3 = 10;//可以给const修饰的引用赋予字面量
//const修饰符修饰的引用的原理
//编译器会把上面的代码变为:int tmp=10;const int &ref3=tmp;
//ref3 = 200;err
//bool类型
//bool类型定义的变量只有两个值,true和false,真和假,1和0
bool is = 0;//注意:is的值除0以外,都是真
if(is)
{
cout << "真" << endl;
}
else
{
cout << "假" << endl;
}
system("pause");
return EXIT_SUCCESS;
}
6.将引用用作函数参数
引用经常用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递函数参数的方法被称为按引用传递。
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//2022年10月19日22:49:15
#include <iostream>
void swapr(int &a, int &b);//a,b是整数的别名
void swapp(int *p, int *q);//p,q是整数地址
void swapv(int a, int b);//a,b是新变量
int main()
{
using namespace std;
int wallet1 = 300;
int wallet2 = 350;
cout << "wallte1 = $" << wallet1;
cout << ", wallet2 = $" << wallet2 << endl;
cout << "Using references to swap contents:\n";
swapr(wallet1, wallet2);//传递变量
cout << "wallte1 = $" << wallet1;
cout << ", wallet2 = $" << wallet2 << endl;
cout << "Using pointers to swap contents:\n";
swapp(&wallet1, &wallet2);//传递变量
cout << "wallte1 = $" << wallet1;
cout << ", wallet2 = $" << wallet2 << endl;
cout << "Tring to using passing by value:\n";
swapv(wallet1, wallet2);//传递变量
cout << "wallte1 = $" << wallet1;
cout << ", wallet2 = $" << wallet2 << endl;
system("pause");
return EXIT_SUCCESS;
}
void swapr(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
void swapp(int *p, int *q)
{
int tmp;
tmp = *p;
*p = *q;
*q = tmp;
}
void swapv(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
输出结果:
wallte1 = $300, wallet2 = $350
Using references to swap contents:
wallte1 = $350, wallet2 = $300
Using pointers to swap contents:
wallte1 = $300, wallet2 = $350
Tring to using passing by value:
wallte1 = $300, wallet2 = $350
请按任意键继续. . .
7.引用的属性和特别之处
程序:
//cubes.cpp--普通和引用参数
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//2022年10月20日19:34:29
#include <iostream>
using namespace std;
double cube(double a);
double refcube(double &ra);
int main()
{
double x = 3.0;
cout << cube(x);
cout << " = cube of " << x << endl;
cout << refcube(x);
cout << " = cube of " << x << endl;
system("pause");
return EXIT_SUCCESS;
}
double cube(double a)
{
a *= a * a;
return a;
}
double refcube(double &ra)
{
ra *= ra * ra;
return ra;
}
输出结果:
27 = cube of 3
27 = cube of 27
请按任意键继续. . .
refcube()函数修改了main()中的值,而cube()没有,这提醒我们为何通常按值传递。变量a位于cube()中,它被初始化为x的值,但修改a并不会影响x。但由于refcube()使用了引用参数,因此修改ra实际上就是修改x。如果程序员的意图是函数使用传递给他的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。
当数据比较大(如结构和类)时,引用参数将很有用。
将参数传递给引用参数的函数,传递引用的限制更严格。如果ra是一个变量的别名,则实参应是该变量。下面的代码不合适,因为表达式x+3.0并不是变量:
double z = refcube(x + 3.0);
例如,不能将值赋给该表达式
x + 3.0 = 5.0;
临时变量、引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。下面来看何种情况下,C++将生成临时变量,以及为何对const引用的限制是合理的。
首先,什么时候将创建临时变量?如果引用参数是const,则编译器将在下面两种情况下生成临时变量:
■实参的类型正确,但不是左值;
■实参的类型不正确,但可以转换为正确的类型。
左值参数,是可以被引用的数据对象。
double refcube(const double &ra)
{
return ra * ra * ra;
}
double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = {2.0, 5.0, 10.0, 12.0};
double c1 = refcube(side);//ra is side
double c2 = refcube(lens[2]);//ra is lens[2]
double c3 = refcube(rd); //ra is rd is side
double c4 = refcube(*pd);//ra is *pd is side
double c5 = refcube(edge);//ra is temporary variable
double c6 = refcube(7.0); //ra is temporary variable
double c6 = refcube(side + 10.0); //ra is temporary variable
参数side、lens[2]、rd和*pd都是有名称的、double类型的数据对象,因此可以为其创建引用,而不需要临时变量(数组元素的行为与同类型的变量类似)。然而、edge虽然是变量,类型却不正确,double引用不能指向long。另一方面,参数7.0和side+10.0的类型都正确,但是没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在C++标准正是这样做的(在默认情况下,有些编译器将发出警告,而不是错误信息,需注意)。
现在来看refcube()函数。该函数的目的只是使用传递的值,而不是修改它们,因此临时变量不会造成任何不利的影响,反而会使函数在可处理的参数种类方面更通用。因此,如果声明将引用指定为const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
应尽可能使用const
将引用参数声明为常量数据的引用的理由有三个:
■使用const可以避免无意中修改数据的编程错误;
■使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
■使用const引用使函数能够正确生成并使用临时变量。
8.将引用用于结构
引用非常适合用于结构和类(C++的用户定义类型)。引入引用的主要目的是为了用于这些类型的,而不是基本的内置类型。
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//2022年10月20日20:41:20
#include <iostream>
#include <string>
using namespace std;
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
void display(const free_throws &ft);//显示对象的成员值
void set_pc(free_throws &ft);//设置percent
free_throws &accumulate(free_throws &target, const free_throws &source);
int main()
{
//部分初始化,剩下的成员设置为0
free_throws one = {"Ifelsa Branch", 13, 14};
free_throws two = {"Andor Knott", 10, 16};
free_throws three = {"Minie Max", 7, 9};
free_throws four = {"Whily Looper", 5, 9};
free_throws five = {"Long Long", 6, 14};
free_throws team = {"Throwgoods", 0, 0};
//不初始化
free_throws dup;
set_pc(one);//设置percent
display(one);//显示对象的成员值
accumulate(team, one);
display(team);
//在参数中使用返回值
accumulate(accumulate(team, two), four);
display(team);
//在参数中使用返回值
dup = accumulate(team, five);
std::cout << "Displaying team:\n";
display(team);
std::cout << "Display dup after assignment:\n";
display(dup);
set_pc(four);
//不明智的分配
accumulate(dup, five) = four;
std::cout << "Displaying dup after ill-advised assignment:\n";
display(dup);
system("pause");
return EXIT_SUCCESS;
}
void display(const free_throws &ft)//显示对象的成员值
{
cout << "Name: " << ft.name << '\n';
cout << " Made: " << ft.made << '\t';
cout << "Attempts: " << ft.attempts << '\t';
cout << "Percent: " << ft.percent << '\n';
}
void set_pc(free_throws &ft)//设置percent
{
if( ft.attempts != 0 )
ft.percent = 100.0f * float(ft.made) / float(ft.attempts);
else
ft.percent = 0;
}
free_throws &accumulate(free_throws &target, const free_throws &source)
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
输出结果:
Name: Ifelsa Branch
Made: 13 Attempts: 14 Percent: 92.8571
Name: Throwgoods
Made: 13 Attempts: 14 Percent: 92.8571
Name: Throwgoods
Made: 28 Attempts: 39 Percent: 71.7949
Displaying team:
Name: Throwgoods
Made: 34 Attempts: 53 Percent: 64.1509
Display dup after assignment:
Name: Throwgoods
Made: 34 Attempts: 53 Percent: 64.1509
Displaying dup after ill-advised assignment:
Name: Whily Looper
Made: 5 Attempts: 9 Percent: 55.5556
请按任意键继续. . .
1.程序说明
由于display()显示结构的内容,而不修改它,因此这个函数使用了一个const引用参数。就这个函数而言,也可按值传递,但与复制原始结构的拷贝相比,使用引用可节省时间和内存。
如果返回类型被声明为free_throws而不是free)throws &,上述返回语句将返回target(也就是team)的拷贝。但返回类型为引用,这意味着返回的是最初传递给accumulate()的team()对象。
2.为何要返回引用
传统返回机制与按值传递传递函数参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
double m = sqrt(16.0);
上述中,值4.0被复制到一个临时位置,然后复制给m。
dup = accumulate(team, five);
如果accumulate()返回一个结构,而不是指向结构的引用,将整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,将直接把team复制到dup,其效率更高。
注意:返回引用的函数实际上是被引用的变量的别名。
3.返回引用时需要注意的问题
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。避免编写如下代码:
const free_throws & clone2(free_throws & ft)
{
free_throws newguy;//犯了严重的错误
newguy = ft;
return newguy;//返回引用去复制
}
该函数返回一个指向临时变量的引用,函数运行完毕后它将不再存在。为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数的数据,因此返回的引用也将指向这些参数。
另一种方法是用new来分配新的存储空间。前面见过这样的函数,它使用new为字符串分配空间,并返回指向该内存空间的指针。
const free_throws & clone(free_throws & ft)
{
free_throws * pt;
* pt = ft;//复制信息
retutrn *pt;//返回引用去复制
}
4.为何将const用于引用返回类型
程序清单8.6包含如下:
accumulate(dup, five) = four;
其效果如下:首先将five的数据添加到dup中,再使用four的内容覆盖覆盖dup的内容。这条语句为何能够通过编译呢?在赋值语句中,左边必须是可以修改的左值。也就是说,在赋值表达中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向dup的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。
另一个方面,常规(非引用)返回类型是右值---不能通过访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。
假如要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,只需将返类型声明为const引用:
const free_throws &accumulate(free_throws &target, const free_throws &source);
9.将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。
程序:
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//2022年10月22日19:20:33
//8.7strquote.cpp--不同的设计
#include <iostream>
#include <string>
using namespace std;
string version1(const string &s1, const string &s2);
const string &version2(string &s1, const string &s2);//有额外效果
const string &version3(string &s1, const string &s2);//不好的设计
int main()
{
string input;
string copy;
string result;
cout << "Enter a string: ";
getline(cin, input);
copy = input;
cout << "Your string as entered: " << input << endl;
result = version1(input, "***");
cout << "Your string enhanced: " << result << endl;
cout << "Your orginal string: " << input << endl;
result = version2(input, "###");
cout << "Your string enhanced: " << result << endl;
cout << "Your orginal string: " << input << endl;
cout << "Reseting orginal string.\n";
input = copy;
result = version3(input, "@@@");
cout << "Your string enhanced: " << result << endl;
cout << "Your orginal string: " << input << endl;
return 0;
}
string version1(const string &s1, const string &s2)
{
string temp;
temp = s2 + s1 + s2;
return temp;
}
const string &version2(string &s1, const string &s2)//有附加效果
s1 = s2 + s1 + s2;
//安全地返回传递给函数的引用
return s1;
}
const string &version3(string &s1, const string &s2)//不好的设计
{
string temp;//临时变量
temp = s2 + s1 + s2;
//返回传递给局部变量的引用是不安全的
return temp;
}
输出结果:
Enter a string: It's not my fault.
Your string as entered: It's not my fault.
Your string enhanced: ***It's not my fault.***
Your orginal string: It's not my fault.
Your string enhanced: ###It's not my fault.###
Your orginal string: ###It's not my fault.###
Reseting orginal string.
Your string enhanced:
Your orginal string: It's not my fault.
它存在一个致命的缺陷:返回一个指向version3()中的变量的引用。这个函数能够通过编译(但编译器会发出警告),但当程序试图执行该函数时将崩溃。具体地说,问题是下面的赋值语句引发的:
result = version3(input, "@@@");
10.对象、继承和引用
ofstream对象可以使用ostream类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承。ostream是基类,ofstream是派生类。派生类继承了基类的方法,这意味着ofstream对象可以使用基类的特性。
继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。
程序清单8.8
#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//2023年2月9日09:39:30
//filefunc.cpp -- 带有ostream引用参数的函数
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
void file_it(ostream& os, double fo, const double fe[], int n);
const int LIMIT = 5;
int main()
{
ofstream fout;
const char * fn = "ep-data.txt";
fout.open(fn);
if( !fout.is_open() )
{
cout << "Can't open " << fn << ". Bye.\n";
exit(EXIT_FAILURE);
}
double objective;//焦距
cout << "Enter the focal length of your "
<< " telescope objective in mm:\n";
cin >> objective;
double eps[LIMIT];
cout << "Enter the focal lengths, in mm, of " << LIMIT
<< " eyepieces:\n";
for (int i = 0; i < LIMIT; i++)
{
cout << "Eyepiece #" << i + 1 << ": ";
cin >> eps[i];
}
file_it(fout, objective, eps, LIMIT);
file_it(cout, objective, eps, LIMIT);
cout << "Done\n";
system("pause");
return EXIT_SUCCESS;
}
void file_it(ostream& os, double fo, const double fe[], int n)
{
os << "Focal length of objective: " << fo << " mm\n";
os << "f.1. eyepiece " << "magnification" << endl;
for (int i = 0; i < n; i++)
{
os << " " << fe[i] << " " << int(fo / fe[i] + 0.5) << endl;
}
}
输入:
Enter the focal length of your telescope objective in mm:
1800
Enter the focal lengths, in mm, of 5 eyepieces:
Eyepiece #1: 33
Eyepiece #2: 55
Eyepiece #3: 22
Eyepiece #4: 66
Eyepiece #5: 44
输出:
Focal length of objective: 1800 mm
f.1. eyepiece magnification
33 55
55 33
22 82
66 27
44 41
Done
请按任意键继续. . .
11.何时使用引用参数
1.使用引用参数的主要原因有两个。
■程序员能够修改调用函数中的数据对象。
■通过传递引用而不是调用整个数据对象,可以提高程序的运行速度。
2.对于传递的值而不做修改的函数
■如果数据对象很小,如内置数据类型或小型结构,则按值传递
■如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
■如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制到结构所需的时间和空间。
■如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
3.对于修改调用函数中数据的函数:
■如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
■如果数据对象是数组,则只能使用指针。
■如果数据对象是结构,则使用引用或指针。
■如果数据对象是类对象,则使用引用。
参考资料
参考资料来源于黑马程序员、C++ Primer Plus、C++ Primer等