当前位置: 首页 > news >正文

电子商务网站建设内容上海环球金融中心观光厅

电子商务网站建设内容,上海环球金融中心观光厅,柳州网站建设哪家便宜,韩国女篮出线了吗0.博客园链接 博客的最新内容都在博客园当中#xff0c;所有内容均为原创(博客园、CSDN同步更新)。 C知识点集合 1.命名空间 在往后的C编程中#xff0c;将会存在大量的变量和函数#xff0c;因为有大量的变量和函数#xff0c;所以C的库会非常多。那么在C语言编程中所有内容均为原创(博客园、CSDN同步更新)。 C知识点集合 1.命名空间 在往后的C编程中将会存在大量的变量和函数因为有大量的变量和函数所以C的库会非常多。那么在C语言编程中如果我们不熟悉库当中有什么东西那么将会产生莫名其妙的错误 #include stdio.h #include stdlib.hint rand 10; int main() {printf(%d\n, rand);return 0; } 1.1命名空间的定义 在上面的例子中stdlib.h中有一函数rand而我们并不知道其中有这个函数而是直接定义了一个名为rand的整形变量此时就会造成重定义。这就是一种命名冲突的表现。 那么在C中为了弥补这方面的不足诞生出了命名空间这么一个东西。先简单看看命名空间是如何定义的 定义命名空间需要用到namespace关键字在其之后要跟上命名空间的名字(随便取)然后再接一对大括号({})大括号中可以定义命名空间的成员。 namespace ly// 在全局域定义一个命名空间 {// 在命名空间中可以定义变量、类型、函数...int x 3;struct Student{};void func(){} }// 不会与命名空间的成员发生冲突 int x 6; struct Student {};int main() {return 0; } 命名空间它不会修改其成员的生命周期例如上面的代码当中命名空间中的变量x、类型Student、函数func他们的生命周期都是跟随程序的。命名空间就好像一道警戒线将其中的成员保护起来 也就是说即使在同一作用域下定义与命名空间中相同名字的变量(或类型或函数)也不会造成冲突。那么命名空间的作用就是提供一个新的限定域这个限定域与作用域有所区别作用域指的是在当前域下的成员只能工作在本域范围内而限定域不会修改成员的生命周期只是防止在同一作用域下相同名字的成员引发的命名冲突。(例如上面的代码在全局域中定义了多个相同名字的成员但因为有命名空间的保护不会造成命名冲突)。 1.2命名空间的使用 我们要想使用命名空间中的成员有三种方式 1.在使用某个成员时在其之前加上[命名空间名::成员名]。其中::为作用域限定符。 #include stdio.h namespace ly// 在全局域定义一个命名空间 {// 在命名空间中可以定义变量、类型、函数...int x 3;struct Student{};void func(){printf(ly::func()\n);} }// 不会与命名空间的成员发生冲突 int x 6; struct Student {}; void func() {printf(func()\n); }int main() {int x 9;printf(%d\n, x);//使用x时编译器从当前开始网上查找xprintf(%d\n, ::x);//::指定在全局域中找xprintf(%d\n, ly::x);//使用命名空间中的xstruct Student s1;// 使用全局域的Student类型定义变量struct ly::Student s2;// 使用全局域的ly命名空间中的Student类型定义变量func();// 调用全局域的func函数ly::func();// 调用全局域中ly命名空间中的func函数return 0; } 2.使用[using namespace 命名空间名]将命名空间中的所有成员释放。 #include stdio.h namespace ly {void func(){printf(ly::func()\n);} }using namespace ly;// 将命名空间中的成员释放 int main() {func();// 直接使用命名空间中的成员return 0; } 3.使用[using 命名空间名::成员名]将命名空间中的某一成员释放。 #include stdio.h namespace ly {int x 3;void func(){printf(ly::func()\n);} }using ly::x;// 将命名空间中的x释放 int main() {printf(%d\n, x);ly::func();// 未释放的成员必须用::访问return 0; } 1.3命名空间定义的补充 命名空间只能在全局域中定义但可以定义任意次 namespace ly {int x 3; }namespace ly {int y 5; }namespace ly {int z 9; }int main() {//namespace ly// 错误局部域不允许定义//{// int m 8;//}return 0; } 这些多次重复定义的命名空间会在编译阶段自动合并。那么C将其标准库里面的东西全部封在了一个名为std的命名空间当中当我们把多个头文件引入源文件时编译器在预处理阶段将这些头文件展开然后在编译阶段合并这些名为std命名空间。 命名空间可以嵌套定义即使嵌套定义相同名称的命名空间也不会触发语法错误(正常人应该不会这么干) #include stdio.h namespace ly {int x 3;namespace lll{int x 6;}namespace ly{int x 4;} }int main() {printf(%d\n, ly::x);printf(%d\n, ly::lll::x);printf(%d\n, ly::ly::x);// 双兔傍地走,安能辨我是雄雌?return 0; } 命名空间在工程当中是常用的模块化编程手段通常发生在项目组协作完成项目时组与组之间互相不知道定义了什么变量、函数、类型而使用命名空间能够有效解决命名冲突的问题进而提升工作效率。 2.输入与输出 有了命名空间的铺垫我们现在才能严格意义上写出第一个C程序 #include iostream// 我们的第一个C标准库 using namespace std;int main() {cout Hello World! endl;return 0; } 这段程序编译运行之后能够在控制台上输出Hello World!字符串(Windows下使用Visual Studio 2022)。其中cout我们称为标准输出对象是的它是一个对象(C是一门面向对象编程的语言)运算符我们称为流插入运算符。其中cout中的c代表英文console(翻译为控制台)cout可以理解为控制台输出。我们每想要输出一个对象(可以是变量、字符串等等)到控制台上在对象之前都必须使用运算符也就是说每一个想要输出到控制台上的对象都必须匹配一个流插入运算符。endl表换行(end line结束当前行)。 我们再对此程序做一个小小的修改 #include iostream// 我们的第一个C标准库 using namespace std;int main() {char buffer[64] { 0 };cin buffer;cout buffer endl;return 0; } 这段程序如同C语言使用scanf函数从控制台获取一个字符串到buffer中去(遇到空格为截止)然后再将buffer里的数据输出到控制台。cin我们称为标准输入对象其中运算符我们称为流提取运算符也就是说每一个想要从控制台获取某些数据的对象都必须匹配一个流提取运算符。 我们需要注意虽然上面的和在C中被赋予了新的定义但它不与重载了或运算符的对象(先别管这里大概懂我意思就行)一起使用时它就保留了原来的功能 #include iostream using namespace std;int main() {int x 3;x x 1;//这里还是位移运算符cout x endl;//这里便是流插入运算符return 0; } 我们应该注意一个非常有趣的现象cout和cin貌似并不需要我们指定任何对象的类型它似乎天然地知道我们的对象类型(自动识别类型)不再需要像C语言当中需要指定%d、%c、%f等等格式。这里我无法做出解释但是请你不要放弃请继续往后看相信你一定会有答案。当然C提供的输入输出是可以支持浮点数的精度控制、格式控制的但我并不建议大家使用(有这闲工夫干嘛不直接printf)。 相信读者一定注意到了C的头文件没有.h后缀但是C并不是天然这样设计的。其实在早期的C中头文件也是需要添加.h后缀的不过这就绕到了我们本篇开头的话题——命名冲突因为C早期的设计初衷就是弥补C语言的缺陷与不足所以当C标准委员会发现了这个问题之后就开始升级C了 由此命名空间就诞生了为了新旧版本的头文件区分索性直接将头文件需要.h后缀的写法给去掉了。不过在一些较为上古的编译器中(例如VC6.0)C的头文件是可以添加.h后缀的当然现在的编译器都不支持这种写法了(即使支持也不建议用)。 3.缺省参数 缺省参数是用在函数声明或定义时为函数的参数指定一个缺省值缺省值也叫默认值。当调用了指定缺省值的函数时且调用时没有实参那么函数的参数的值将使用缺省值而如果调用了指定缺省值的函数时但调用时指定了实参那么函数的参数将使用实参。 #include iostream using namespace std;void func(int x 3) {cout x endl; } int main() {func();// 没有指定实参func函数的x参数使用它的缺省值func(5);// 指定了实参func函数的x参数使用这个实参return 0; } 3.1全缺省参数 顾名思义全缺省参数指的是函数的每个参数都有一个缺省值。假设有一参数为三个的函数它的每个参数都有缺省值那么在调用它时可以选择传递0个实参、1个实参、2个实参、3个实参。 #include iostream using namespace std;void func(int a 10, int b 20, int c 30) {cout a a ;cout b b ;cout c c endl; } int main() {func();func(100);func(100, 200);func(100, 200, 300);return 0; } 需要注意的是函数调用的实参和函数的参数的位置是一一对应的。也就是说实参传递给函数的参数时一定是从左往右传递的中间不可跳过。举一个很简单的例子我们想要函数参数a和c使用实参而参数b使用缺省值很抱歉这是不可能的。以下图来体会实参和函数的参数的对应关系 3.2半缺省参数 半缺省参数指的是函数的形参至少有一个参数没有缺省值并且没有缺省值的参数必须是在有缺省值参数的左边。 // 没有缺省值的参数在有缺省值参数的左边 void func1(int a, int b 20, int c 30)// 正确半缺省 {cout a a ;cout b b ;cout c c endl; }// 没有缺省值参数在有缺省值参数的右边 void func2(int a 10, int b, int c 30)// 错误半缺省 {cout a a ;cout b b ;cout c c endl; }// 没有缺省值参数在有缺省值参数的右边 void func3(int a 10, int b 20, int c)// 错误半缺省 {cout a a ;cout b b ;cout c c endl; } 3.3缺省参数的补充 非常值得注意的一点是在函数的声明和定义中缺省参数不能同时出现。因为一旦同时出现编译器就会陷入纠结引发报错。在函数既有声明又有定义的场景中C规定只能在函数声明中给定缺省参数。 #include iostream using namespace std;void func(int x 6);// 只能在声明中给定缺省参数int main() {func();return 0; }void func(int x) {cout x endl; } 还需要注意缺省值必须是常量或全局变量。 那么缺省参数到底有何意义我们以一个简单的例子想必就能明白 struct Stack {int* a;int size;int capacity; };// 如果我们确实不清楚要把多少个数据存入栈中 // 初始化时就以4开始后面慢慢扩容 void capacity_init(struct Stack* st,int cap 4) {//...做一些扩容的工作st-capacity cap;// ... }int main() {struct Stack st;capacity_init(st);// 不确定存多少个数据使用缺省值然后扩容capacity_init(st, 100);// 如果我们已经确定要存100个数据,就不需要再扩容增加消耗了return 0; } 4.函数重载 在C语言当中同名的函数只能定义一个。但是我们会有这么一种需求不同类型的参数都要进行同一份动作而C语言不允许同名函数的存在那么痛苦就来了 int add(int x, int y) {return x y; }double add_double(double x, double y) {return x y; }int main() {int a 1, b 4;add(a, b);// 两个整数可以调用add函数相加double x 6.6, y 9.9;add(x, y);// 两个浮点数也可以但我们不想有任何精度损失应该怎么办// 只能重新定义一个与add函数不冲突的函数了add_double(x, y);return 0; } 那么我们试想int类型、long long类型、float类型、double类型、char类型......都需要相加时那对函数起名就是一个庞大的任务了。所以为了应付这种情况C诞生出了函数重载。 函数重载是函数的一种特殊情况C允许在同一作用域中定义多个功能类似的同名函数这些函数之间的区别就是形参列表的参数个数、参数类型或参数顺序不同(本质就是要类型不同)。我们来看C是怎么应对上面那种情况的 void swap(int* x, int* y) {// ... }void swap(double* x, double* y) {// ... }void swap(char* x, char* y) {// ... } int main() {int a 3, b 5;swap(a, b);// 调用 void swap(int* x,int* y);double x 7.2, y 3.4;swap(x, y);// 调用 void swap(double* x,double* y);char n a, m z;swap(n, m);// 调用 void swap(char* x,char* y);return 0; } 当然了函数重载应付这种情况不是最优解最优解应该是函数模板待我们介绍到模板时上面的那些swap、add只需要写一份。函数重载的最大用处不在这里在往后的学习过程中能够体会到。 下面介绍如何设计函数实现函数重载 1.保证参数的类型不同 // 参数的类型不同可以构成重载 void func(int x, int y) {cout func(int,int) endl; }void func(char x, char y) {cout func(char,char) endl; }void func(int x, char y) {cout func(int,char) endl; } 2.保证参数的个数不同 // 保证参数的个数不同可以构成重载 void test(int x, int y) {cout test(int,int) endl; }void test(int x) {cout test(int) endl; }void test() {cout test() endl; } 3.保证参数的顺序不同 // 参数的顺序不同可以构成重载 // 不过一定要注意是类型的顺序不同! void count(int x, double y, char z) {cout count(int,double,char) endl; }void count(double x, int y, char z) {cout count(double,int,char) endl; } 以上三条规则希望大家牢记于心这三条规则实际上就是要保证重载函数之间的类型互不相同。在函数调用时编译器会根据传入的实参来确定调用哪个函数。当然了编译器是如何匹配合适的函数的我这里就不写了因为它又臭又长真的不好写有兴趣的可以参考C圣经——C Primer。 同时我们需要注意函数调用的二义性关于这部分编译器是不会出现编译错误的这类问题通常是程序员的失误 #include iostream using namespace std;// 两个重载func函数符合函数重载定义他们是没有问题的 void func(int x 3, int y 4) {cout x x y y endl; }void func(const char* str hello world) {cout str endl; }int main() {func();// 但是调用时存在二义性return 0; } 4.1C为什么支持函数重载 这里需要提到一个名词函数名修饰规则(不是我们为函数起的名字而是编译器为函数取的名字)。需要普及一下当C/C程序在编译阶段完成后形成汇编代码调用函数的语句都会转化为汇编指令以[call:函数地址]的形式调用函数。那么问题就出在这里了我们以一份C/C通用代码在Linux环境下观察他们的汇编代码 C/C程序代码 int add(int x, int y) {return x y; } int main() {add(3, 5);return 0; } 同一份代码分别放在.c文件和.cpp文件中然后再分别以gcc、g编译可以得到不同的汇编代码。我们可以看到gcc对add函数的修饰非常简单我们取什么名字gcc也对该函数取什么名字也就是说当我们定义两个相同名字的函数时gcc就傻了而g这边就有所不同它把add函数修饰成了_Z3addii其中_Z3代表我们取的名字长度为3(我们取的名字叫add)然后再跟上我们的取的函数名add最后再跟上ii每一个i都代表一个int。也就是说当前定义了一个名为add的函数其中两个参数都为int当我们再定义一个名为add的函数但是参数是int和char类型那么g修饰之后的函数名为_Z3addic因为与_Z3addii存在区别所以g依然能够分辨到底该调用哪个函数。所以在C中可以依靠函数参数的类型不同从而实现函数重载。 那么还有一个至关重要的问题为什么函数的返回类型不能够确定重载 int add(int x, int y) {return x y; }double add(int x, int y)// 错误 {return x y; } 我们要注意我们在调用函数的时候是体现不出返回类型的。调用函数的方法都是[函数名(参数)]这种格式即使我们使用一个变量接收它的返回值但仍然不属于函数调用的部分。即使g将函数的返回类型作为函数名修饰的一个参数但还是因为函数调用时不能确定返回类型g还是无法做出选择。 .引用 引用是给一个已存在的变量取一个别名。编译器不会为引用变量开辟内存空间引用变量与被引用的变量共用同一块内存空间。这就好比说水浒传人物李逵他的本名叫李逵但我们也可以叫他黑旋风也就是说李逵就是黑旋风黑旋风就是李逵。那么在上对引用变量操作就是对被引用的变量操作。 #include iostream using namespace std;int main() {int a 3;int ra a;// 引用变量引用已经存在的变量ara;// 对引用变量操作就是对被引用的变量操作cout a a ra ra endl;return 0; } 可见引用的语法就是[类型名 引用变量名(对象名) 被引用的实体]。但同时也要注意引用变量的类型和被引用的变量的类型必须是同种类型哪怕它们能互相发生隐式类型转换 int main() {int x 3;double rx x;// 错误即使int与doubke能够相互转换return 0; } 那么在语言层面上我们可以如下图这样理解引用(注意我说的是语言层面上) 同时希望大家注意用词准确我们常说的变量实际上指的是内存空间例如上图使用int类型开辟出来的4字节空间而变量名指的是我们为这块空间所取的名字例如上图的a。 5.1引用特性 在使用引用时需要注意以下几个特性 1.引用在定义时必须被初始化 int main() {int r;// 错误引用一旦被定义它必须被初始化int x 3;int rx x;//正确用法return 0; } 2.一个变量(对象)可以有多个引用也可以发生连续引用 int main() {int x 3;// 一个变量可以被引用多次int rx1 x;int rx2 x;// 引用之间可以连续引用int rx3 rx1;int rx4 rx3;return 0; } 3.引用一旦引用了一个实体他便不能再去引用其他实体(我们的本意是引用另一个实体实际上发生的是赋值) #include iostream using namespace std; int main() {int z 6;int rz z;int w 9;rz w;// 我们的本意是改变rz的引用实体但实际上发生的是赋值cout z endl;return 0; } 5.2引用的使用场景 引用的使用场景不是在一个作用域中取别名玩来玩去它的用法通常用做函数参数、函数返回值 1.引用做参数在没有接触引用之前我们定义的swap函数的两个参数都是指针类型。现在我们接触了引用可以写成下面这样 #include iostream using namespace std;void swap(int left, int right) {int tmp left;left right;right tmp; } int main() {int x 3;int y 7;swap(x, y);cout x x ,y y endl;return 0; } 具体分析一下这段代码 像这样的传参方式我们把它称为引用传参。与传值传参不同的是传值传参会发生一次拷贝而引用不发生拷贝也就是说引用传参能够提高一些程序效率。 2.引用做返回值在介绍引用返回之前先来了解传值返回会发生什么 int func() {static int x 0;x;return x; }int main() {int ret func();return 0; } 在这个例子中func函数定义了一个静态变量其名为x在执行return语句的时候x的值由0递增到1。那么这个时候我们需要注意return x并不是把x变量返回而是func函数的返回值类型为int会生成一个临时变量x变量把值拷贝到这个临时变量当中外部的ret变量接收func函数的返回值实际上是临时变量再次把里面的值拷贝到ret变量当中 而如果我们以引用做返回值那么中间的过程会与传值返回有所区别 int func() {static int x 0;x;return x; }int main() {int ret func();return 0; } 如果我们将接收func函数返回值的整形变量换成引用变量那么将再减少一次拷贝的过程。 由此可以看出引用无论是做参数还是做返回值中间过程都能减少空间开辟、拷贝所带来的消耗从而提高工作效率。但是当我们对上面的程序稍做修改就会产生一个小错误 #include iostream using namespace std;int func() {int x 0;x;return x; }int main() {int ret func();cout ret endl;return 0; } 在这个例子中当主函数调用func函数时会创建func函数对应的函数栈此时x变量不再存储在数据段而是存放在函数栈中也就是说当func函数完成工作之后其申请的函数栈会被销毁x变量也会随之销毁。注意我们的销毁打了双引号我想说的是这个销毁并不是内存直接被销毁而是函数退出之后其原先申请的内存便不属于我们了。而我们接收func函数返回值的变量是一个引用变量也就是说ret引用了一块不属于我们的内存但是当前程序依然能够正确输出结果 但是这并不意味我们的程序没有错误因为我们的场景是在是太简单了如果我们将程序修改地稍微复杂一些 #include iostream using namespace std;int func() {int x 0;x;return x; }void test() {int x 100; }int main() {int ret func();cout ret endl;cout ret endl;test();cout ret endl;return 0; } 它的输出结果可能会令人出乎意料(这段程序是放在Visual Studio 2013下编译运行的) 现在我们对产生这个奇怪的结果作出解释并且总结一些关于引用的结论 1.正常输出1的原因当主函数调用func函数func函数结束时主函数当中ret引用变量引用了一块不属于我们的空间。我们能看到正常输出1仅仅是一个巧合说明编译器、操作系统没有初始化、占用这块空间而是保留了原来的数据。那么ret引用了正确的值它本身被当作参数传递给cout(暂时这么理解反正输出语句是个函数)那么在屏幕上正常输出1就可以理解了。 2.输出一个随机值的原因这是第二个输出语句的输出结果。其实道理很简单第一个输出语句是一个函数那么它被调用时就会创建对应的函数栈在创建函数栈的过程当中因为某些原因(通常是函数栈开辟需要一些参数)ret引用的那块空间被随机值覆盖了此时ret引用的那块空间的值就发生了改变。然后再将ret作为参数传递给第二条输出语句就在屏幕上打印随机值了。 3.输出100的原因在第三条输出语句之间还调用了一次test函数。运气好的是test的函数栈和func的函数栈的大小是一样的(真正开辟空间的有效语句就一条)也就是说test函数中有一名为x的变量其值为100这个x变量恰好开辟在了ret所引用的空间当中其值恰好覆盖了ret所引用的空间。所以ret的值就被覆盖成了100传递给第三条输出语句在屏幕上打印100。 我们上面一直在介绍一个错误程序我想提醒大家的是内存空间的销毁并不意味着内存空间不存在了它是一直存在的只不过它不受任何保护可以被任何数据修改我们依然能够访问那块空间但是访问到的数据是不确定的例如上面的那段错误程序我们可能访问到正确的值也可能访问到一个随机值也可能访问到一个属于其他函数的变量值。再其次强调一下引用的用法上面的那段错误程序的复杂程度是很低的如果我们在以后的开发过程中滥用引用会造成难以排查的BUG也是一种基础不扎实的表现所以要把引用当作返回值的开发场景当中一定要确保引用的对象出了函数作用域不销毁。 我们以一段程序证明上面的错误程序中的ret引用变量一直引用同一块空间 #include iostream using namespace std;int func() {int x 0;x;return x; }void test() {int x 100; }int main() {int ret func();cout ret endl;cout ret endl;test();cout ret endl;return 0; } 5.3引用传参和引用返回对效率的影响 引用传参和引用返回都能减少临时变量的开辟、数据拷贝的次数从而在一定程度上提升代码的运行效率。但是在现代的计算机硬件体系当中这些细微的效率差距我们是体会不出来的所以下面的代码尽可能复现引用对效率的影响 #include iostream using namespace std; #include time.h// 这个结构体就有 4w字节 struct A {int arr[10000]; };struct A a;// 定义一个全局变量struct A func1()// 传值返回 {return a; }struct A func2()// 引用返回 {return a; }int main() {// 计算传值返回的时间差size_t beign1 clock();for (int i 0; i 100000; i)// 调用10W次传值返回的函数{func1();}size_t end1 clock();// 计算引用返回的时间差size_t begin2 clock();for (int i 0; i 100000; i){func2();}size_t end2 clock();cout struct A func1(): end1 - beign1 endl;cout struct A func2(): end2 - begin2 endl;return 0; } 从输出结果来看传值返回的函数调用10万次用时208ms引用返回的函数调用10万次用时2ms可见在这个场景当中引用比传值的效率高出了100倍。 其实引用做不做参数、做不做返回值无关紧要重要的是当引用做参数时它可以做输出型参数外部用引用接受函数的返回值时返回值可以被修改(关于这部分的用法在往后的内容会被频繁使用)。 5.4常引用 下面的程序是否正确 int main() {const int y 7;int ry y;return 0; } 这段程序是错误的。原因在于y变量被定义的本意就是让它只能被初始化不能被赋值也就是说我们的本意是让y变量在以后的场景当中保持它的初值我们对y的权限只能读不能写。但是紧跟着的ry引用变量却违背了这个初衷ry引用的变量我们认为能够对其进行读写操作而这段代码当中ry引用了一个只能读不能写的变量此时就会产生一个冲突。所以编译器严厉制止这样的行为这种情况我们称为权限放大(我懒得写什么顶层const底层const有兴趣的去看C Primer)。在C中权限只能被平移或缩小不能被放大。我们对上面的代码做出修改 int main() {const int y 7;const int ry y;//权限平移int z 3;const int rz z;//权限缩小return 0; } 常引用的权限虽然只能读但它不会影响被引用的变量的权限 int main() {int x 3;const int rx x;// x被常引用引用x;// 但是不会修改其权限// x语句执行完后x的值为4rx的值也为4return 0; } 那么我们回到本小节开头谈到的引用变量的类型和被引用的变量的类型必须是同种类型哪怕它们能互相发生隐式类型转换但是使用常引用可以使得两边类型不相同(能够相互发生类型转换的类型) int main() {double x 3.14;const int rx x;return 0; } 这个时候我们必须探究一下这种现象是为什么。首先发生类型转换的变量不会引起它自身的变化而是生成一个类型转换后的临时变量再将变量的值拷贝到临时变量当中。我们以一个程序以及画图来说明这个问题 #include iostream using namespace std;int main() {double x 3.14;cout (int)x endl;cout x endl;// 上一条语句x已经发生类型转换但是它本身不变return 0; } 那么这段代码我们就可以解释了 int main() {double x 3.14;const int rx x;return 0; } 在这段代码当中rx引用变量并没有引用x变量而是引用了double类型变量x向int类型转换过程中产生的临时变量。也就是说临时变量具有常性临时变量的生命周期仅限于程序的当前行。那么我们需要注意函数的参数(传值传参的参数也叫形参)并不是临时变量而是函数在创建函数栈时预先开辟好的栈空间也就是说形参的生命周期跟随函数(函数栈销毁形参也销毁)。 我们再次研究原来的某一段程序只不过这次稍微做了些修改请读者注意 #include iostream using namespace std;int func()// 注意这里是传值返回 {int x 0;x;return x; }int test()// 这里也是传值返回 {int x 100;return x; }int main() {const int ret func();cout ret endl;cout ret endl;test();cout ret endl;return 0; } func函数以传值返回的方式返回外部使用常引用接收其返回值这是正确的做法。但是在这里会有一个耐人寻味的问题这段程序的打印结果为什么全是正确的这里我们不得不解释一下函数的返回值存放在哪里我们就以上面这段程序来解释 图(1)描述了函数返回返回值、函数外部接收返回值的过程图(2)描述了一个函数要调用某一函数之前编译器会根据需要确定被调用函数的返回值类型在调用被调用函数的函数栈中开辟足够的空间用来存放返回值也就是说ret引用变量引用了跟自己生命周期一样的一块空间这就没有违背不要去引用不属于自己的空间的原则又因为这块空间并不是用户主动开辟的所以它具有常属性。所以在随后调用test函数时会发生同样的事所以就不存在空间覆盖问题。 同时在这里多嘴一句在使用引用传参的时候也需要注意函数调用的二义性(当有函数重载时) // 下面两个Add函数构成重载这是没有问题的 int Add(int x, int y) {return x y; }int Add(int x, int y) {return x y; }int main() {Add(1, 2);//匹配第一个Add因为第二Add的参数是普通引用引用不了常量int x 3, y 4;Add(x, y);// 这里就有问题了x、y都是变量调用任何一个Add函数都可以所以存在二义性return 0; } 5.6引用和指针的区别 虽然说引用能做到的事指针也能做到但在C中引用不能够完全代替指针(据我所知java好像用引用代替指针了)这是因为C中给引用的定义就与其他语言不一样我们下面列举引用与指针的不同点 1.引用变量实质上是为一个已存在的变量取一个新的名字它本身不开辟空间而指针变量能够存储空间的地址它具有空间大小 2.引用变量在定义时必须被初始化而指针可以不初始化 3.引用变量在引用了一个实体之后便不能再引用其他实体而指针变量可以在任何时候改变指向的实体(改变存储的空间地址) 4.没有空引用但有空指针 5.在sizeof运算符中的含义不同。sizeof(引用变量)的计算结果为被引用变量的大小(对引用的操作就是对被引用对象的操作)但sizeof(指针变量)计算的是指针变量的大小 6.对引用的操作会递增被引用变量的值而指针而是按照类型向后偏移对应的字节数 7.有多级指针但不存在多级引用 8.访问实体的方式不同。指针访问实体需要解引用而引用由编译器自动处理 9.引用使用起来比指针更加安全 以上列举的都是语言层面的不同实际上在底层它们两个都一样。也就是说引用在底层实际上会开辟空间引用就是由指针设计而来。我们观察下面这段程序的汇编代码 int main() {int a 10;int ra a;int b 3;int* pb b;return 0; } 6.auto关键字 auto关键字是C11的一个关键字在这之前auto是用来声明局部变量的。我们在.c文件中执行下面这段代码(我懒得弄旧版本的编译器用C语言文件代替一下) int main() {auto int x 3;int y 6;return 0; } 那么到了C11标准委员会发现没人会像上面这样使用auto于是彻底将以前的功能删除取而代之的新功能是自动类型推导。需要自动类型推导的场景常常发生在类型难于拼写、类型含义不明确而导致的出错使用auto可以解决这些问题。我们以下面的代码为例体会auto的用法 #include vector #include stringint main() {std::vectorstd::string vec;//创建一个存储string的vector对象std::vectorstd::string::reverse_iterator it1 vec.rbegin();//it1前面是类型auto it2 vec.rbegin();// 使用auto推导it2的类型return 0; } 由此不难推断出在我们定义某一变量时auto使用变量的初始化数据的类型来确定该变量的类型。 也就是说使用auto定义变量时必须对其进行初始化编译器会在编译阶段根据初始化表达式(等号右边的变量类型)来推导auto的实际类型所以auto并不是一种具体类型它实际上是一个类型声明的占位符(告诉编译器这里有一个类型不知道是什么等待编译器推导)。 #include iostream using namespace std;int main() {auto a 10;// 10为int类型所以a为int类型auto b a;// a为int类型所以b为int类型auto c (double)b;// b类型转换后为double类型所以c为double类型double rc c;auto cc rc;//cc为double类型并不是double类型auto p1 a;//a为int*类型所以p1为int*类型auto* p2 a;// 与上面等价// 输出各变量类型的名称cout typeid(a).name() endl;cout typeid(b).name() endl;cout typeid(c).name() endl;cout typeid(cc).name() endl;cout typeid(p1).name() endl;cout typeid(p2).name() endl;return 0; } 因此auto类型的变量不能不初始化 int main() {auto x;//错误auto修饰的变量必须被初始化auto y 3;// 正确用法return 0; } 那么使用auto在一行定义多个变量时需要保证这些变量的类型是相同的 int main() {auto a 1, b 2, c 3;auto x 1.1, y 3.14, z c;//错误return 0; } auto即使非常有用但是并不是绝对有用在下面两个场景当中就不能使用auto 1.auto不能做函数参数 void func(auto i 10) {cout typeid(i).name() endl; } 在编译阶段主要的工作就是检查语法然后生成汇编代码这个阶段并没有发生函数调用(函数栈可以说是编译器创建的具体体现在汇编代码当中CPU执行这些指令就创建函数栈了所以函数调用发生在程序运行时)这就意味着函数没有收到外部实参没有收到实参就意味着参数的值不确定值不确定就意味着类型不确定即使我们上面的func函数写了缺省值但缺省值是在调用函数时不给实参的情况下才起作用的。所以编译器在编译阶段无法确定auto要推导的参数的类型。 2.auto不能用来直接声明数组 int main() {int a1[] { 1, 2, 3, 4, 5 };auto a2[] { 2, 3, 4, 5, 6 };//错误用法return 0; } 在C/C中数组算不上很严格的数据类型我们平常用C/C编程时不会说这是个数组类型。以下面一段代码就可以说明这个问题 #include iostream using namespace std;int main() {int arr[5] {1,2,3,4,5};cout sizeof(arr) endl;// 输出20很合理//int arr2[5] arr;// 这样的赋值是错的int* parr arr;// 这样才是对的cout sizeof(parr) endl;//32位平台return 0; } 其实在C11当中{}已经是一个类型了(initializer_listT感兴趣的可以去cplusplus看看)也就是说auto不给声明数组但我们可以这么玩 #include initializer_list #include iostream using namespace std;int main() {//auto il[] { 1, 2, 3, 4, 5 };//C不给这么玩auto il { 1, 2, 3, 4, 5 };//我们这么玩cout typeid(il).name() endl;return 0; } 这段代码没啥意义initializer_list的玩法也不是这么玩的主要是想告诉大家注意auto声明数组是错的但是声明initializer_list是正确的(万一哪天做选择题碰到了这个陷阱呢)。 7.范围for 范围for是C11提供的一种新式的遍历方式。如果我们在C98当中要遍历一个数组那么我们会这么干 #include iostream using namespace std;int main() {int arr[] { 1, 2, 3, 4, 5, 6, 7 };for (int i 0; i sizeof(arr) / sizeof(int); i){cout arr[i] ;}cout endl;return 0; } 但我们在C11当中使用范围for #include iostream using namespace std;int main() {int arr[] { 1, 2, 3, 4, 5, 6, 7 };for (int e : arr){cout e ;}cout endl;return 0; } 范围for的用法非常简单for后面接一对小括号小括号里面的内容是[迭代的变量:迭代的范围]。通俗的讲:右边的是我们要遍历的目标:左边的是一个变量(变量的名字随意这里我写了e)范围for会将:右边的遍历目标当中的每一个元素赋值给:左边的变量。 如我们想要修改遍历目标当中的某一元素我们可以使用引用 #include iostream using namespace std;int main() {int arr[] { 1, 2, 3, 4, 5, 6, 7 };for (int e : arr){e;cout e ;}cout endl;return 0; } 当然了如果在某些场景场中我们懒得写目标元素的类型我们可以配合auto使用 #include iostream using namespace std;int main() {int arr[] { 1, 2, 3, 4, 5, 6, 7 };for (auto e : arr){e;cout e ;}cout endl;return 0; } 这是范围for的基本用法范围for的目的不仅仅是为了遍历数组它的底层是用迭代器实现的在我们介绍到STL的时候读者就能有所了解。范围for更大的用处是方便遍历STL中的容器。 在这里我要强调一点范围for遍历的目标一定是具有固定范围的数组、容器等等(凡是有迭代器都可以遍历)。当数组作为实参传递给函数时函数参数就不是数组类型了而是指针类型那么对指针使用范围for去遍历是错误的也是让人捉摸不透的 void func(int arr[]) {for (auto e : arr)// 错误arr不是数组{} } int main() {int arr[] { 1, 2, 3, 4, 5 };func(arr);return 0; } 8.指针空值nullptr nullptr也是C11新出的一个关键字这个关键字出来的意义是为了填坑。在C11之前代表空指针的关键字为NULL但是它存在一个歧义NULL是一个宏但是这个宏所表示的常量不是一个指针类型而是整数类型其值为0。这就注定了在某些场景当中一定存在歧义 #include iostream using namespace std;void func(int x) {cout void func(int) endl; }void func(void* p) {cout void func(void*) endl; }int main() {func(NULL);// 我们的本意是传一个指针类型但实际上匹配的函数的参数不是指针类型return 0; } 我们可以在传统C头文件stddef.h当中找到关于NULL的宏 #ifndef NULL #ifdef __cplusplus #define NULL   0 #else #define NULL   ((void *)0) #endif #endif 那么C11的nullptr就是一个真正的指针类型 #include iostream using namespace std;void func(int x) {cout void func(int) endl; }void func(void* p) {cout void func(void*) endl; }int main() {func(nullptr);return 0; } 在C当中我认为应当尽量使用nullptr。那么C为什么不把NULL删除呢原因在于语言的设计应当向后兼容也就是说以前的东西即使不正确也应当保留因为某些开发者已经利用了这种特性。具体为什么要向后兼容读者可以自行搜索Python2和Python3的故事。 9.内联函数 以关键字inline修饰的函数就成为内联函数。内联函数的功能是在编译时直接展开该函数而不是等到函数调用时再创建函数栈。内联函数和宏的差别在于 1.内联函数在编译时展开而宏则是在预处理时展开 2.内联函数展开做的工作是嵌入到程序当中而宏只是简单的文本替换 3.内联函数是函数它可以被调试而宏不可被调试 4.宏不具有类型安全 我们先使用宏函数的方式支持两个整数相加 // 下面哪个宏函数是最合理的 #define Add(x,y) x y #define Add(x,y) (x)(y) #define Add(x,y) ((x)(y)); #define Add(x,y) ((x)(y)) 答案是第四个我们来具体分析一下 关于宏函数我只是提醒一下如果要使用的话一定要注意写法要正确。那么证明内联函数在编译时直接展开呢我们可以通过观察汇编代码来侧面证明如果汇编语句存在call就说明这个函数实实在在被调用并且创建函数栈如果不存在call就说明这个函数是内联函数。我们在编译器的debug模式下调试这段代码(Visual Studio 2013)观察它的反汇编 inline int Add(int x, int y) {return x y; }int main() {int sum Add(1, 2);return 0; } 发现内联函数并没有起什么作用不要着急配置一下项目属性 1.点击菜单栏的项目并选择属性 2.在C/C选项中点击常规并把调试信息格式修改为程序数据库(/Zi) 3.在C/C选项中选择优化找到启用内部函数将其修改为只适用于 __inline(/Ob1) 然后我们在观察反汇编代码这次发现汇编代码没有call语句了。由此说明内联函数确实会在编译时展开。 不过inline关键字对于编译器来说是一个建议性的请求编译器根据需要选择忽略或同意。我们将Add函数的代码的长度增长一些再观察反汇编代码 inline int Add(int x, int y) {int sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;sum x y;return sum; }int main() {int sum Add(1, 2);return 0; } 由此可以说明使用inline修饰一些代码较多、规模较大的函数时编译器可能会忽略内联函数的请求。内联函数适用于代码较少、规模较小且被频繁调用的函数。例如我们常用的swap函数就可以用内联函数来实现。内联函数的意义实际上是以空间换时间的一种展开策略展开的代码会增加可执行程序的大小但是该程序在执行中调用函数时不需要创建函数栈。注意内联函数不能是递归函数。但是在递归函数前面加inline也不会报错因为inline是一个建议型的关键字 inline int test(int n)// 不会报错 {if (n 0){return 0;}return test(n - 1); }int main() {test(3);return 0; } 最重要的是内联函数在多文件的开发环境当中不能够分离编译(函数声明单独在头文件函数定义单独在源文件)否则会引发链接错误 // func.h #pragma onceinline void func(); // func.cpp #include func.hvoid func() {// ... } // main.cpp #include func.hint main() {func();// 有调用才会链接错误没有调用不会报错return 0; } 既然是链接错误就说明语法没有问题而编译器报错信息为无法解析的外部符号就说明链接器找不到名为func的函数这就说明在编译阶段一定出现了问题下面来解析这个问题 所以得出一个重要的结论是编译器不会让带有inline关键字的函数进入符号表所以内联函数不能分离编译。 在介绍函数重载的时候探讨过C为什么能够支持函数重载一个重要的原因就是因为函数名的修饰规则不同那么再综合刚才学到的知识可以推断出C中每一个重载的函数都会进符号表发生调用时都去符号表寻找对应的被调用函数因为C对函数名的特殊修饰从而实现函数重载。
http://www.hkea.cn/news/14305241/

相关文章:

  • 网站流量查询站长之家在线教育网站开发
  • asp网站建设代码汕头建设工程总公司
  • 网站建设读书笔记网页商城设计商城网站设计案例
  • phpcms 怎么做视频网站网站可分为哪两种类型
  • 网站设计公司网站佛山专业建站公司哪家好
  • 临桂区建设局网站导航网站后台源码
  • 上海域邦建设集团网站设计制作图片
  • 那些网站做推广php网站空间支持
  • 南昌制作网站的公司wordpress主题付费下载
  • 百度云做网站空间华龙网重庆
  • 池州做网站培训深圳搜索优化
  • 云南省建设厅网站人员查询百度推广一年收费标准
  • 中国建设银行官网站诚聘英才福安网站定制
  • 网站开发前景如何域名注销期间网站还能打开吗
  • 用jsp做网站的感想win7安装wordpress
  • 牡丹江网站seo网站建设怎么找客源
  • 电子商务网站建设与实践上机指导网站流量如何赚钱
  • 网站优化 seo和sem电商网站有哪些使用场景
  • 温州教育网站建设青岛网站搜索排名
  • 怎么知道网站用什么软件做的江门众瞬网络科技有限公司
  • 设计与制作网站郑州电力高等专科学校面试问题
  • 做石油系统的公司网站php网站开发技术前景
  • 太原网站推广只选中联传媒wordpress 中文cms主题
  • 郑州网站制作案例自己做网站服务器
  • 无锡网站优化价格脑洞大开的创意设计
  • 团购网站单页模板销售管理crm
  • 建立有域名网站功能wordpress+编辑器字号
  • 网站制作风格白杨seo
  • 网站意见反馈源码老专家个人网站
  • destoon 网站后台技术网站模版