网站搭建流程图,ui基础教程入门,安溪人做的网站,国内org域名的网站文章目录 一、引用1.1 引用概念1.2 引用特性1.3 常引用1.4 使用场景1.4.1 做参数1.4.2做返回值 1.5 引用和指针的区别1.6 小结一下 二、内联函数2.1 内联的概念2.2 内联的特性2.3 【面试题】 三、auto关键字(C11)3.1 类型别名思考3.2 auto简介 四、auto的使用细则4.1 基于范围的… 文章目录 一、引用1.1 引用概念1.2 引用特性1.3 常引用1.4 使用场景1.4.1 做参数1.4.2做返回值 1.5 引用和指针的区别1.6 小结一下 二、内联函数2.1 内联的概念2.2 内联的特性2.3 【面试题】 三、auto关键字(C11)3.1 类型别名思考3.2 auto简介 四、auto的使用细则4.1 基于范围的for循环(C11)4.2 范围for的使用条件 五、指针空值nullptr(C11) 一、引用
1.1 引用概念 C是C语言的继承它可进行过程化程序设计又可以进行以抽象数据类型为特点的基于对象的程序设计还可以进行以继承和多态为特点的面向对象的程序设计。引用reference就是C对C语言的重要扩充。引用就是某一变量目标的一个别名对引用的操作与对变量直接操作完全一样。引用的声明方法类型标识符 引用名目标变量名 --百度百科 这个引用就相当于是别名
void TestRef()
{int a 10;int ra a;//定义引用类型printf(%p\n, a);printf(%p\n, ra);
}类型 引用变量名(对象名) 引用实体 我们通过调试来看一下 1.2 引用特性
引用在定义时必须初始化一个变量可以有多个引用引用一旦引用一个实体再不能引用其他实体
void main()
{int a 10;// int ra; // 该条语句编译时会出错,必须要初始化int ra a;int rra a;printf(%p %p %p\n, a, ra, rra);
}1.3 常引用
取别名不能放大权限
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; ax的结果是一个临时变量临时变量具有常性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; // 这样写不可以这里const加上就可以将不同类型取别名类型转换的时候会出现一个临时变量**临时变量具有常性所以就可以~**
double d 1.1;
int i d; // 强制类型转换
int ri d; // 无法赋予类型不同
const int ri d; // 加上const就可以了被引用的实体不能是常量引用的类型必须相同 1.4 使用场景 引用有两个场景分别是做参数和做返回值 1.4.1 做参数
做参数【在我们C语言阶段的时候使用函数交换两个变量的值就需要传地址过去否则的话形参只是实参的一份临时拷贝】
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的引用对指针使用比较复杂的场景进行一些替换让代码更简单易懂但是不能完全替代指 针引用不能完全替代指针原因引用定义后不能改变指向 就比如说在数据结构中学的链表指针需要改变指向引用不能改变指向这就是引用不能代替指针的原因 那java和pythone等其他语言有没有指针–没有那他们的链表是怎么实现的呢–引用本质上就是引用可以改变指向 我们再来看一个案例在我们学数据结构的时候单链表学习阶段有这这么一段代码这里的pphead必须要传二级指针有点不好理解呀
void PushBack(struct Node** pphead, int x)
{*pphead newnode;
}
int main()
{struct Node* plist NULL;return 0;
}而我们学了引用就可以这样写了加上一个引用也就是phead是plist的一份临时拷贝
void PushBack(struct Node* phead, int x)
{phead newnode;
}int main()
{struct Node* plist NULL;return 0;
}传值、传引用效率比较
我们这里可以测试一下性能对比一下传值、传引用
#include time.hstruct 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;
}
可以看到传值需要8毫秒而传引用小于0毫秒小于0毫秒这里显示不出来所以显示的0 那我再次测试一下以引用作为函数的返回值类型和以值作为函数的返回值类型的性能
#include time.h
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;
}这就不用我说了吧~~ 以值作为参数或者返回值类型在传参和返回期间函数不会直接传递实参或者将变量本身直接返回而是传递实参或者返回变量的一份临时的拷贝因此用值作为参数或者返回值类型效率是非常低下的尤其是当参数或者返回值类型非常大时效率就更低 1.4.2做返回值
首先来看这段代码这段代码是将a的值返回然后ret接收没有什么问题
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;
}在调用完一次后再次调用栈帧大小是一样的会复用前面的空间 结论返回变量出了函数作用域生命周期就销毁了不能用引用返回
那什么情况下可以用引用返回呢 全局变量/静态变量/堆上的变量等就可以用引用返回 那么我这里举例用一个现实中的场景
首先来看一下这里已经c和c混着写了
#includeassert.h
// 升级成类了直接就可以使用名字不用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;// 获取每偶数进行*2for (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;
}首先这里C写有变化
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;
}上面的代码C首先将C语言的结构体升级成了类了然后函数可以定义到类里面了在使用的时候获取值就要注意一个点必须返回引用之前不是说不能用引用返回码这里就不一样了这个malloc出来的空间是在堆上的所以可以返回返回别名后修改修改别名也就是修改原来的地址…end
1.5 引用和指针的区别
对比区别的话要从两个维度来对比一个是语法一个是底层这两个不要混在一起了 语法层面理解 来看下面的这段代码
int main()
{int a 10;int ra a; // 语法不开空间ra 20;int* pa a; // 语法上开空间*pa 20;return 0;
}引用是别名不开空间指针是地址需要开空间这里我们上面都已经知道了~ 底层理解 我们来看一下汇编代码 看到在底层是开空间的引用底层是指针实现的语法含义和底层实现是背离的
1.6 小结一下
语法层面上
引用是别名不开空间指针是地址需要开空间引用必须初始化指针可以初始化也可以不初始化引用不能改变指向指针可以引用相对更安全没有空引用但是有空指针容易出现野指针但是不容易出现野引用sizeof 、、解用访问等方面的区别
底层层面上【汇编】 汇编层面上没有引用都是指针引用编译后也转换成指针了
引用…End
二、内联函数
2.1 内联的概念 以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销内联函数提升程序运行的效率。 假设我要频繁调用100w次建立100w个栈帧
int Add(int a, int b)
{return a b;
}c语言如何解决这个问题的宏函数
#define ADD(a, b) ((a)(b))核心点宏是预处理阶段进行替换
宏的缺点 1、语法复杂坑很多不容易控制 2、不能调试 3、没有类型安全的检查
这个时候C就引入了一个概念inline就是把函数的运算逻辑放到里面来不进行建立栈帧
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;
}如果在上述函数前增加inline关键字将其改成内联函数在编译期间编译器会用函数体替换函数的调用
查看方式
在release模式下查看编译器生成的汇编代码中是否存在call Add在debug模式下需要对编译器进行设置否则不会展开(因为debug模式下编译器默认不会对代码进行优化下面是vs2022的设置) 在不加内联的情况下 添加了内联后可以看到有很大的特别 我们再来看一个场景就是分文件定义的时候我就想在.h文件中定义一个函数 然而我在使用的时候两个文件都包含了这个头文件在编译阶段链接的时候就会报链接错误冲突的原因就是两个文件都会生成符号表进行链接有同名会冲突因为是同一个函数这咋办啊~ 我们有三种解决方案 第一种就是声明和定义分离
这个不多说基本都会之前我们用的都是这样的方法
第二种方式就是static修饰函数链接属性只在当前文件可见
在C语言阶段详细大家知道滴~~ 第三种方式就是加入内联函数
这里的内联函数相当于也就是static 这里要注意内联修饰的函数比较大他是不会展开的小函数就会展开所以小函数使用内联大函数使用静态 2.2 内联的特性
inline是一种以空间换时间的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率。inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议将函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下图为《Cprime》第五版关于inline的建议 inline 不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。 2.3 【面试题】
宏的优缺点
优点
增强代码的复用性。提高性能。
缺点
不方便调试宏。因为预编译阶段进行了替换导致代码可读性差可维护性差容易误用。没有类型安全的检查 。 C有哪些技术替代宏
常量定义 换用const enum短小函数定义 换用内联函数 三、auto关键字(C11)
3.1 类型别名思考
随着程序越来越复杂程序中用到的类型也越来越复杂经常体现在
类型难于拼写含义不明确导致容易出错
int main()
{std::mapstd::string, std::string m{ { apple, 苹果 }, { orange,橙子 },{pear,梨} };std::mapstd::string, std::string::iterator it m.begin();while (it ! m.end()){//....}return 0;
}大家来上面的一个代码有可能看不懂但是我们只需要知道std::mapstd::string, std::string::iterator是一个类型但是该类型太长了特别容易写错。聪明的同学可能已经想到可以通过typedef给类型取别名比如
typedef std::mapstd::string, std::string Map;使用typedef给类型取别名确实可以简化代码但是typedef有会遇到新的难题
typedef char* pstring;int main()
{const pstring p1; const pstring* p2; return 0;
}在编程时常常需要把表达式的值赋值给变量这就要求在声明变量的时候清楚地知道表达式的 类型。然而有时候要做到这点并非那么容易因此C11给auto赋予了新的含义。
这个时候auto就有作用了 3.2 auto简介 在早期C/C中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量但遗憾的是一直没有人去使用它大家可思考下为什么 C11中标准委员会赋予了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;
}这里的typeid(函数名).name()就是打印类型 就刚刚上面的代码就可以这样写了
#include string
#include map
int main()
{std::mapstd::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声明指针类型时用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不能推导的场景
auto不能作为函数的参数
// 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}auto不能直接用来声明数组
void TestAuto()
{int a[] {1,2,3};auto b[] {456};
}为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法auto在实际中最常见的优势用法就是跟以后会讲到的C11提供的新式for循环还有 lambda表达式等进行配合使用。
4.1 基于范围的for循环(C11) 范围for的语法 在C98中如果要遍历一个数组可以按照以下方式进行
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;
}对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因此C11中引入了基于范围的for循环。for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。
void TestFor()
{int array[] { 1, 2, 3, 4, 5 };for (auto e : array)e * 2;for (auto e : array)cout e ;
}注意与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。9.2 范围for的使用条件
4.2 范围for的使用条件 for循环迭代的范围必须是确定的 对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。
注意以下代码就有问题因为for的范围不确定
void TestFor(int array[])
{for(auto e : array)cout e endl;
}迭代的对象要实现和的操作。(关于迭代器这个问题以后会讲现在提一下没办法讲清楚现在大家了解一下就可以了)
五、指针空值nullptr(C11)
在良好的C/C编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现不可预料的错误比如未初始化的指针。如果一个指针没有合法的指向我们基本都是按照如下方式对其进行初始化
void TestPtr()
{int* p1 NULL;int* p2 0;// ……
}NULL实际是一个宏在传统的C头文件(stddef.h)中可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif或者我们右键转到定义就可以看到 可以看到NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量。不论采取何种定义在使用空值的指针时都不可避免的会遇到一些麻烦比如
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因此与程序的初衷相悖。 在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void*)0。
注意
在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的。在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。 本文章重点介绍了引用内联函数的注意事项以及使用提了一下auto关键字和空指针问题能看完的烙铁相信已经学会了最后请多多指教如有疑问请在评论区或私信交流~~