C++是C语言的继承,它可进行过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。引用(reference)就是C++对C语言的重要扩充。引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名; -->百度百科
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
void main()
{
int a = 10;
// int& ra; // 该条语句编译时会出错,必须要初始化
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
int main()
{
int a = 0;
// 权限的缩小
const int& c = a;
const int x = 10;
// 权限的放大
int& y = x;
return 0;
}
const int x = 10;
const int y = x;
a+x
的结果是一个临时变量,临时变量具有常性,const
引用就可以int& n = a + x;
的返回值是临时变量,临时对象具有常性,是一个权限放大int a = 0;
const int x = 10;
const int& z = 10;
const int& m = a + x; //这样写也可以
int& n = a + x; // 这样写不可以
double d = 1.1;
int i = d; // 强制类型转换
int& ri = d; // 无法赋予,类型不同
const int& ri = d; // 加上const就可以了
- 被引用的实体不能是常量
- 引用的类型必须相同
引用有两个场景分别是做参数和做返回值
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int x = 0, y = 1;
Swap(&x, &y);
return 0;
}
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 0, y = 1;
Swap(x, y);
return 0;
}
指针和引用的功能是类似的,有重叠的
C++的引用,对指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替代指
就比如说在数据结构中学的链表,指针需要改变指向,引用不能改变指向,这就是引用不能代替指针的原因
void PushBack(struct Node** pphead, int x)
{
*pphead = newnode;
}
int main()
{
struct Node* plist = NULL;
return 0;
}
void PushBack(struct Node*& phead, int x)
{
phead = newnode;
}
int main()
{
struct Node* plist = NULL;
return 0;
}
传值、传引用效率比较
#include
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void main()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
#include
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void main()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int func()
{
int a = 0;
return a;
}
int main()
{
int ret = func();
cout << ret << endl;
return 0;
}
int& func()
{
int a = 0;
return a;
}
int main()
{
int ret = func();
cout << ret << endl;
return 0;
}
我们以前说指针有野指针,那么引用也有野引用
上面的代码在func函数里是将a的别名返回了,函数调用完会销毁,这里与函数的栈帧的创建与销毁有关
在调用完函数后,那块空间会被销毁,然后再访问被销毁的地址,会造成野引用
栈帧销毁的时候可能被清理,结果可能是随机值,但是在vs上是不清理的
我们可以证明一下
int& func()
{
int a = 0;
return a;
}
int& fx()
{
int b = 1;
return b;
}
int main()
{
int& ret = func();
cout << ret << endl;
fx();
cout << ret << endl;
return 0;
}
结论:返回变量出了函数作用域生命周期就销毁了,不能用引用返回
全局变量/静态变量/堆上的变量等就可以用引用返回
那么我这里举例用一个现实中的场景:
#include
// 升级成类了,直接就可以使用名字,不用typedef了
struct SeqList
{
int* a;
int size;
int capacity;
};
void SLInit(SeqList& sl)
{
sl.a = (int*)malloc(sizeof(int) * 4);
// ..
sl.size = 0;
sl.capacity = 4;
}
void SLPushBack(SeqList& sl, int x)
{
//...扩容
sl.a[sl.size++] = x;
}
// 修改
void SLModity(SeqList& sl, int pos, int x)
{
assert(pos >= 0);
assert(pos < sl.size);
sl.a[pos] = x;
}
int SLGet(SeqList& sl, int pos)
{
assert(pos >= 0);
assert(pos < sl.size);
return sl.a[pos];
}
int main()
{
SeqList s;
SLInit(s);
// 这里接收的是引用,所以我们就不需要取地址了
SLPushBack(s, 1);
SLPushBack(s, 2);
SLPushBack(s, 3);
SLPushBack(s, 4);
for (int i = 0; i < s.size; i++)
{
cout << SLGet(s, i) << " ";
}
cout << endl;
// 获取每偶数进行*2
for (int i = 0; i < s.size; i++)
{
int val = SLGet(s, i);
if (val % 2 == 0)
{
SLModity(s, i, val * 2);
}
}
cout << endl;
for (int i = 0; i < s.size; i++)
{
cout << SLGet(s, i) << " ";
}
cout << endl;
return 0;
}
struct SeqList
{
// 成员变量
int* a;
int size;
int capacity;
// 成员函数
void Init()
{
a = (int*)malloc(sizeof(int) * 4);
// ...
size = 0;
capacity = 4;
}
void PushBack(int x)
{
// ... 扩容
a[size++] = x;
}
// 读写返回变量
// 临时变量具有常性
// 所以必须返回引用,引用中间没有产生临时变量,是一个别名
int& Get(int pos)
{
assert(pos >= 0);
assert(pos < size);
return a[pos];
}
};
int main()
{
SeqList s;
s.Init();
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
s.PushBack(4);
for (int i = 0; i < s.size; i++)
{
cout << s.Get(i)<< " ";
}
cout << endl;
for (int i = 0; i < s.size; i++)
{
if (s.Get(i) % 2 == 0)
{
s.Get(i) *= 2;
}
}
cout << endl;
for (int i = 0; i < s.size; i++)
{
cout << s.Get(i) << " ";
}
cout << endl;
return 0;
}
语法层面理解:
int main()
{
int a = 10;
int& ra = a; // 语法不开空间,
ra = 20;
int* pa = &a; // 语法上开空间
*pa = 20;
return 0;
}
底层理解:
语法层面上:
底层层面上:【汇编】
汇编层面上,没有引用,都是指针,引用编译后也转换成指针了
引用…End
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
int Add(int a, int b)
{
return a + b;
}
#define ADD(a, b) ((a)+(b))
宏的缺点:
1、语法复杂,坑很多,不容易控制
2、不能调试
3、没有类型安全的检查
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret1 = Add(1, 2) * 3;
int x = 1, y = 2;
int ret2 = Add(x | y, x & y);
return 0;
}
查看方式:
我们有三种解决方案:
第一种就是声明和定义分离
第二种方式就是static修饰函数,链接属性,只在当前文件可见
第三种方式就是加入内联函数
宏的优缺点?
优点:
缺点:
C++有哪些技术替代宏?
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
std::map::iterator
是一个类型但是该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:typedef std::map<std::string, std::string> Map;
使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:
typedef char* pstring;
int main()
{
const pstring p1;
const pstring* p2;
return 0;
}
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的
类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
如下所示:
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a; // 整形
auto c = 'a'; // 字符类型
auto f = &a; //指针类型
auto d = TestAuto(); // 函数指针类型
// auto e; error 必须要对其进行初始化
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(f).name() << endl;
return 0;
}
#include
#include
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
auto it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
【注意】
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
用auto声明指针类型时,用auto和auto*没有任何区别,但用 auto 声明引用类型时则必须加 &
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;
}
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
范围for的语法
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
}
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
注意:
本文章重点介绍了引用,内联函数的注意事项以及使用,提了一下auto关键字和空指针问题,能看完的烙铁相信已经学会了,最后请多多指教,如有疑问请在评论区或私信交流~~