饮料公司网站模板,程序员 给老婆做网站,局网站建设方案,建一个个人网站需要多少钱前言#xff1a; 紧接着上两篇文章#xff0c;c入门基础(上)#xff1a;C入门基础(上) c入门基础(中)#xff1a;C入门基础(中) 继续补充完c初阶入门基础的知识点#xff0c;本章知识点包括#xff1a; 引用和指针的区别、内联函数、auto关键字(C11)、基于范围的for循环…前言 紧接着上两篇文章c入门基础(上)C入门基础(上) c入门基础(中)C入门基础(中) 继续补充完c初阶入门基础的知识点本章知识点包括 引用和指针的区别、内联函数、auto关键字(C11)、基于范围的for循环(C11)、指针空值nullptr(C11)等补充知识。 目录
6.引用
6.6 引用和指针的区别
7.内联函数
7.1概念
7.2 特性
8.auto关键字(C11)
8.1 类型别名思考
8.2auto简介
特别注意auto不能推导的场景
记住关键字:typeid
9.基于范围的for循环(C11)
9.1 范围for的语法
10. 指针空值nullptr(C11) 6.引用
6.6 引用和指针的区别
来看下面代码右击鼠标转到反汇编: 可以看到汇编代码大家都是一样的解析下面的汇编代码 dword 双字 就是四个字节 ptr pointer缩写 即指针 []里的数据是一个地址值这个地址指向一个双字型数据 比如mov eax, dword ptr [a] 把内存地址a中的双字型32位数据赋给eax lea表示load effective address表示对某个变量的地址进行加载相当于取地址的操作。所以lea eax, [a]就是将a的地址赋给eax寄存器。
接着分别进行以下操作 一样的转到反汇编 引用和指针的不同点(建议别背从使用的角度区分) 1. 引用概念上定义一个变量的别名指针存储一个变量地址。 2. 引用在定义时必须初始化指针没有要求 3. 引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何 一个同类型实体 4. 没有NULL引用但有NULL指针 5. 在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32 位平台下占4个字节) 6. 引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小 7. 有多级指针但是没有多级引用 8. 访问实体方式不同指针需要显式解引用引用编译器自己处理 9. 引用比指针使用起来相对更安全 7.内联函数
复习一下实现一个ADD的宏函数 #define N 10 以下均为错误案例: #define ADD(int x,int y) {return xy;}
#define ADD( x, y) {return xy;}
#define ADD( x, y) return xy;#define ADD(x,y) xy;
#define ADD(x,y) xy;
#define ADD(x,y) (xy);//特别注意一下这一条为什么错误。 解析上面需要注意那条 从以上可以简单看出宏函数的优缺点
缺点 1、容易出错语法坑很多 2、不能调试 3、没有类型安全的检查 优点 1、没有的类型的严格限制 2、针对频繁调用小函数不需要再建立栈帧提高了效率 引出我们的内联函数也是不需要建立栈帧. 7.1概念 以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数调用建立栈帧的开销内联函数提升程序运行的效率。 如果在上述函数前增加inline关键字将其改成内联函数 在编译期间编译器会用函数体替换函数的调用。 查看方式 1. 在release模式下查看编译器生成的汇编代码中是否存在 call Add 2. 在debug模式下需要对编译器进行设置否则不会展开 ( 因为debug模式下编译器默认不会对代码进行优化以下给出vs2019的设置方式 ) 在debug设置下默认是不会展开的: 在使用内联函数之前需要进行以下设置 展开是什么意思呢就是不用调用函数直接把函数里的功能直接拷贝放到main函数内实现 而且解决了符号优先级的问题因为函数传参是把表达式算出结果再传进去的。 7.2 特性 inline是一种 以空间换时间的做法如果编译器将函数当成内联函数处理在 编译阶段会用函数体替换函数调用缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率 inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建 议将 函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、 不 是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下图为 《Cprime》第五版关于inline的建议 编译器视为内联函数 因代码膨胀编译器不视为内联函数 add是内联函数func被编译器视为不是内联函数。 把func函数类冗余的代码删除 这样就被视作为内联了。
测试代码 #includeiostream
#includevector
#includestring.h#define ADD(x,y) ((x)(y))
inline int add(int x, int y)
{return x y;
}
inline int func()
{int x1 0;int x2 0;int x3 0;int x4 0;int ret 0;ret x1;//以下代码全注释掉/*ret - x2;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;ret - x3;*/return 0;
}
int main()
{ADD(1, 2);printf(%d\n,ADD(1,2));printf(%d\n,ADD(1,2)*3);int a 1, b 2;//ADD(a | b, a b);//a | b ab//int ret add(1, 2);int ret add(a | b, a b);printf(%d\n,ret);ret func();} 声明和定义分离
3. inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址 了链接就会找不到
测试代码
Func.h #pragma once
#includeiostream
using namespace std;
inline void f(int i);void fx(); Func.cpp #includeFunc.h
void f(int i)
{cout f(int i) i endl;
}
void fx()
{//既有声明又有定义,是直接展开f(1);
} main.cpp #include F.h
int main()
{f(1);fx();return 0;
}以上代码执行 无法解析的外部符号void_cdecl f(int)(?fYAXHZ)函数 main 中用了该符号 说明 在一个源文件形成可执行程序的阶段预编译阶段的作用其中之一就是头文件的包含实际上是展开 所以说预编译之后Func.cpp的代码其实是这个样子的 #include iostream
using namespace std;
inline void f(int i);
void f(int i)
{cout i endl;
}void fx()
{f(1);
} 编译器一看f函数是一个内联(inline)在调用的地方直接展开了也就不会建立函数栈帧(不会有那一堆的汇编指令)所以就没有函数地址了也就不会进入符号表。
即Func.cpp的符号表中f函数的地址是无效的。
在链接阶段在这个main.cpp内调用f函数就去查这个符号表发现地址是无效的就会报链接型错误。
(编译链接知识点欠缺的,传送编译链接基础知识上)
图解
改正
如何避免这个链接型错误 那就让内联函数声明和定义不分离:这样的话在预编译阶段就把Func.h内的头文件展开了,这样的话呢在编译阶段就展开了那么在main.cpp的符号表里面就有了f函数的地址在链接阶段通过main函数里面的f()函数调用一查符号表就找到了f函数。 Func.h #pragma once
#includeiostream
using namespace std;inline void f(int i)
{cout f(int i) i endl;
}
void fx();Func.cpp #includeFunc.hvoid fx()
{//既有声明又有定义,直接展开f(1);
} main.cpp #includeFunc.h
#includestdio.h
int main()
{//只有声明cout 内联函数: endl;f(1);cout 调用内联函数的函数: endl;fx();
} 以上代码执行 C有哪些技术替代宏 1. 常量定义 换用const enum 2. 短小函数定义 换用内联函数 8.auto关键字(C11)
8.1 类型别名思考
随着程序越来越复杂程序中用到的类型也越来越复杂经常体现在 1. 类型难于拼写 2. 含义不明确导致容易出错 #include string
#include map
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是一个类型但是该类型太长了特别容易写错。
这时候想到咱们以前学过typdef可以通过typedef给类型取别名比如
#include string
#include map
typedef std::mapstd::string, std::string map;
int main()
{map m{ { apple, 苹果 },{ orange, 橙子 }, {pear,梨} };map::iterator it m.begin();while (it ! m.end()){//....}return 0;
}使用typedef给类型取别名确实可以简化代码但是typedef有会遇到新的难题
typedef char* pstring;
int main()
{const pstring p1; // 编译成功还是失败const pstring* p2; // 编译成功还是失败return 0;
}编译失败因为const pstring p1;这行代码定义了一个常量指针但没有初始化编译器无法确定它指向的具体地址而const pstring* p2;这行代码定义了一个指向常量指针的指针但没有指定指针指向的类型也无法确定它指向的具体地址。 在编程时常常需要把表达式的值赋值给变量这就要求在声明变量的时候清楚地知道表达式的 类型。然而有时候要做到这点并非那么容易因此C11给auto赋予了新的含义 8.2auto简介
在早期C/C中auto的含义是: 使用auto修饰的变量是具有自动存储器的局部变量 C11中标准委员会赋予了auto全新的含义即: auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译时期推导而得。
可以用来自动推导类型
int main()
{int a 0;//正常赋值是这样int b a;
}
用auto之后:
根据右边的值推导出左边的值的类型
int main()
{int a 0;auto b a;//intauto c a;//int*auto d a;//int
}
普通场景没有什么价值,类型很长就有价值简化代码
std::vectorstd::string v;
std::vectorstd::string::iterator it v.begin();
可以简化为
auto it v.begin();
特别注意auto不能推导的场景 1. auto不能作为函数的参数 // 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{} 2. auto不能直接用来声明数组 void TestAuto()
{int a[] {1,2,3};auto b[] {456};
} 3. 为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法 4. auto在实际中最常见的优势用法就是跟以后会讲到的C11提供的新式for循环还有 lambda表达式等进行配合使用。 记住关键字:typeid
C里面有个typeid关键字看变量的实际类型,拿到类型字符串(类型名)
#includeiostream
#includevector
#includestring.h
using namespace std;
int main()
{int a 0;//int b a;auto b a;auto c a;auto d a;cout typeid(c).name() endl;cout typeid(d).name() endl;cout typeid(it).name() endl;return 0;
} 9.基于范围的for循环(C11)
9.1 范围for的语法 在C98中如果要遍历一个数组可以按照以下方式进行
#includeiostream
using namespace std;
int main()
{int array[] { 1, 2, 3, 4, 5 };// 定义一个数组接着用for循环访问之后遍历正常的流程是这样的for (int i 0; i sizeof(array) / sizeof(array[0]); i)array[i] * 2;for (int i 0; i sizeof(array) / sizeof(array[0]); i)cout array[i] ;cout endl;return 0;
}
执行 对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因 此C11中引入了基于范围的for循环。for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。 代码测试:
#includeiostream
using namespace std;
int main()
{int array[] { 1, 2, 3, 4, 5 };// 范围for,依次取数组中的数据赋值给e,自定判断结束,自动迭代for (auto e : array)//范围for一般搭配auto使用可以灵活地适应数组类型的变化{cout e ;}cout endl;
}
代码执行 分析以下代码的问题:预计打印 2 46810
int main()
{int array[] { 1, 2, 3, 4, 5 };for (auto x : array){x * 2;}for (auto e : array){cout e ;}cout endl;//return 0;
}
实际打印 我们知道范围for的作用就是:依次把数组里面的array[0]、array[1]...赋值给x但是x的改变不会影响数组里面的值所以要给一个引用 那这时候的x赋值每一次都是array[0]、array[1]...的别名,修改了x就是修改了数组的值: 注意不能这么写 其他知识后面再讲解。 10. 指针空值nullptr(C11)
我们以之前所学的知识判断以下代码是这样匹配的 但实际上都会匹配到第一个函数 源于c大佬们留下的一个坑
NULL实际是一个宏在传统的C头文件(stddef.h)中可以看到如下代码 #ifndef NULL #ifdef __cplusplus #define NULL 0 // 在c里面是这样定义的 #else #define NULL ((void *)0) //不在c里面就是这样定义的比如说c #endif #endif 程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的初衷相悖。 在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void *)0。 这个坑还不能随便改,形容的比较贴切 c里面为了填补这个坑 注意1.在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的。 2.在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同 3.为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr. c入门基础的知识讲解到此告一段落如有错误欢迎纠正感谢来访。