哪家做网站公司最好,常见的网络推广方法有几种,做网站创意是什么意思,重庆响应式网站设计目录
一.iostream文件
二.命名空间
2.1.命名空间的定义
2.2.命名空间的使用
三.C的输入输出
四.缺省参数
4.1.缺省参数概念
4.2.缺省参数分类
4.3.缺省参数注意事项
4.4.缺省参数用途
五.函数重载
5.1.重载函数概念
5.2.C支持函数重载的原理--名字修饰(name Mangl…目录
一.iostream文件
二.命名空间
2.1.命名空间的定义
2.2.命名空间的使用
三.C的输入输出
四.缺省参数
4.1.缺省参数概念
4.2.缺省参数分类
4.3.缺省参数注意事项
4.4.缺省参数用途
五.函数重载
5.1.重载函数概念
5.2.C支持函数重载的原理--名字修饰(name Mangling)
5.3.extern C
六.引用
6.1.引用的概念
6.2.引用的特性
6.3.引用的使用场景
6.3.1.引用作为函数参数
6.3.2.引用作为函数返回值
6.4.传值和传引用效率比较
值和引用的作为参数的性能比较
值和引用的作为返回值类型的性能比较
6.5.常引用
6.6.引用和指针的区别
七.内联函数
7.1.内联函数定义
7.2.内联函数特性
7.3.内联函数与宏函数的区别
八.auto关键字
8.1.auto简介
8.2.auto的使用细则
8.3.auto不能推导的场景
九.基于范围的for循环
9.1.范围for的语法
9.2.范围for的使用条件
十.指针空值nullptr 前言
C是在C的基础之上容纳进去了面向对象编程思想并增加了许多有用的库以及编程范式 等。熟悉C语言之后对C学习有一定的帮助本章节主要目标
补充C语言语法的不足以及C是如何对C语言设计不合理的地方进行优化的比如作用域方面、IO方面、函数方面、指针方面、宏方面等为后续类和对象学习打基础。
首先我们先来编写一个简单的C程序 #includeiostream
using namespace std;int main()
{cout hello C endl;return 0;
} 接下来针对该程序中的主要语法进行详细讲解。
一.iostream文件
iostream是标准的C头文件在旧的标准C中使用的是iostream.h实际上这两个文件是不同的在编译器include文件夹里它是两个文件并且内容不同。现在C标准明确提出不支持后缀为.h的头文件为了和C语言区分开C标准规定不使用后缀.h的头文件。这不只是形式上的改变其实现也有所不同。
二.命名空间
using namespace std该段代码是引用全局命名空间在讲解全局命名空间之前先来学习一下什么是命名空间。命名空间实际上是由程序设计者命名的内存区域程序设计者可以根据需要指定一些有名字的空间区域把一些自己定义的变量函数等标识符存放在这个空间中从而与其他实体定义分隔开来。
案例分析 #include stdio.hint rand 0;int main()
{printf(%d\n, rand);return 0;
}运行结果 但是当我们加上头文件#includestdlib.h #include stdio.h
#include stdlib.hint rand 0;int main()
{printf(%d\n, rand);return 0;
}运行结果 可以看出 在加上头文件stdlib之后程序却运行出错。究其原因可以发现在头文件stdlib中已经定义了名为rand的函数而编译器又无法区分所打印的rand是函数还是变量所以编译器在运行程序的过程中会提示“rand”重定义最终导致程序运行出错。
在C/C中变量、函数和后面要学到的类都是大量存在的这些变量、函数和类的名称将都存 在于全局作用域中可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化 以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的。
2.1.命名空间的定义 namespace 空间名 {......} namespace是定义命名空间的关键字空间名可以用任意合法的标识符在{ }内声明空间成员例如定义一个命名空间A1代码如下所示 namespace A1
{int a 10;
} 则变量a只在A1空间内({ }作用域)有效命名空间的作用就是建立一些互相分隔的作用域把一些实体定义分隔开来。
正常的命名空间定义 namespace N
{//命名空间中可以定义变量/函数/类型//定义变量int rand 10;//定义函数int Add(int left, int right){return left right;}//定义类型struct Node{struct Node* next;int val;};
} 嵌套的命名空间定义 namespace N1
{int a;int b;int Add(int left, int right){return left right;}namespace N2{int c;int d;int Sub(int left, int right){return left - right;}}
} 同一个工程中允许存在多个相同名称的命名空间编译器最后会合成同一个命名空间中
注意一个工程中的test.h和上面test.cpp中两个N1会被合并成一个 namespace N1
{int Mul(int left, int right){return left * right;}
} 注意
一个命名空间就定义了一个新的作用域命名空间中的所有内容都局限于该命名空间中。
2.2.命名空间的使用
当命名空间外的作用域要使用空间内定义的标识符时有三种方法可以使用
a.用空间名加上作用域标识符“::” 来标识要引用的实体 namespace sql
{namespace A{int rand 0;//定义函数void func(){printf(func()\n);}}
}int main()
{printf(%d\n, sql::A::rand);return 0;
} 在引用处指明变量所属的空间就可以对变量进行操作了。
b.使用using关键字在要引用空间实体的上面使用using关键字引入要使用的空间变量 namespace sql
{namespace A{int sum 0;//定义函数void func(){printf(func()\n);}}
}int main()
{printf(%d\n,sql::A::sum);return 0;
} 这种情况下只能使用using引入的标识符如以上代码中只引入了sum如果sql空间里还有标识符b则b不能被使用但可以使用sql::A::b的形式。
c.使用using关键字直接引入要使用的变量所属的空间 namespace sql
{namespace A{int sum 0;//定义函数void func(){printf(func()\n);}}
}using namespace sql::A;int main()
{printf(%d\n,sum);return 0;
} 但这种情况如果引入多个命名空间往往容易出错例如定义了两个命名空间两个空间都定义了变量a如下所示 namespace A1
{int a 10;
}namespace A2
{int a 20;
}using namespace A1;
using namespace A2;int main()
{printf(%d\n,a);//引起编译错误
} 这样在输出a时就会出错因为A1和A2空间都定义了a变量引入不明确编译出错。因此只有在使用命名空间数量很少以及确保这些命名空间中没有同名成员时才使用using namespace语句。
在编写C程序时由于C标准库中的所有标识符都被定义于一个名为std的namespace中所以std又叫作标准命名空间要使用其中定义的标识符就要引入std空间。
三.C的输入输出
当我们在屏幕上输出“hello C”时读者或许会吃惊为什么不是printf()。其实printf()函数也可以但它是C语言的标准输出函数。在C中输入输出都是以“流”的形式实现的C定义了iostream流类库它包含两个基础类istream和ostream用于表示输入流和输出流并在库中定义了标准输入流对象cin和标准输出流对象cout分别用于处理输入和输出。
cin与提取运算符“”结合使用用于读入用户输入以空白(包括空格回车TAB)为分隔符。
cout与插入运算符“”结合使用用于打印消息。通常它还会与操作符endl使用endl的效果是结束当前行并将与设备关联的缓冲区(buffer)中的数据刷新到设备中保证程序所产生的的所有输出都被写入输出流而不是仅停留在内存中。
注意 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时必须包含 iostream 头文件 以及按命名空间使用方法使用std。
案例 #includeiostream
using std::cout;
using std::endl;int main()
{//:流插入运算符std::cout hello world!\n std::endl;//等价于//std::cout hello world!\n \n;cout hello world!\n \n;int i 11;double d 11.11;printf(%d %lf\n, i, d);//自动识别类型//std::cout i d std::endl;cout i d std::endl;//:流提取scanf(%d%lf,i,d);std::cin i d;std::cout i d std::endl;return 0;
} 运行结果 std命名空间的使用惯例 std是C标准库的命名空间如何展开std使用更合理呢
在日常练习中建议直接using namespace std即可这样就很方便using namespace std展开标准库就全部暴露出来了如果我们定义跟库重名的类型/对象/函数就存在冲突问题。该问题在日常练习中很少出现但是项目开发中代码较多、规模大就很容易出现。所以建议在项目开发中使用像std::cout这样使用时指定命名空间 using std::cout展开常用的库对象/类型等方式。
四.缺省参数
C的函数也支持默认参数机制即在定义定义或声明函数时给形参一个初始值在调用函数时如果不传递实参就使用默认参数数值。
4.1.缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实 参则采用该形参的缺省值否则使用指定的实参。
案例 void Func(int a 0)
{cout a endl;
}int main()
{Func(1);Func(2);Func(3);//当不传实际参数时则用缺省值Func();return 0;
} 运行结果 4.2.缺省参数分类
全缺省参数
全缺省参数是声明或定义函数时为函数的参数全都指定一个缺省值。 void TestFunc(int a 10, int b 20, int c 30)
{cout a a endl;cout b b endl;cout c c endl endl;
}int main()
{//有参数传入它会先从左向右依次匹配TestFunc();//a,b,c使用默认形参TestFunc(1);//只传递1给形参ab,c使用默认形参值TestFunc(1, 2);//传递1给a2给bc使用默认形参TestFunc(1, 2, 3);//传递三个参数不使用默认形参值//TestFunc(,1,);//不可以return 0;
} 运行结果 半缺省参数
半缺省参数是声明或定义函数时为函数的部分参数自右向左连续指定缺省值且中间不能有间隔。 void TestFunc(int a, int b 20, int c 30)
{cout a a endl;cout b b endl;cout c c endl endl;
}int main()
{//TestFunc();//必须传入一个值TestFunc(1);//只传递1给形参ab,c使用默认形参值TestFunc(1, 2);//传递1给a2给bc使用默认形参TestFunc(1, 2, 3);//传递三个参数不使用默认形参值return 0;
} 运行结果 4.3.缺省参数注意事项
a.默认参数只可在函数声明中出现一次如果没有函数声明只有函数定义才可以在函数定义中设定。
b.默认参数定义的顺序是自右向左即如果一个参数设定了默认参数则其右边不能再有普通参数。
c.默认参数调用时遵循参数调用顺序即有参数传入它会先从左向右依次匹配。
d.默认参数值可以是全局变量、全局常量甚至可以是一个函数但不可以是局部变量因为默认参数的值是在编译时确定的而局部变量位置与默认值在编译时无法确定。
4.4.缺省参数用途
在学习数据结构中的栈时当我们在对栈进行初始化过程中并不知道要为该栈开辟多少字节的内存空间起始状态我们都是为该栈开辟4个int类型的空间当栈满时再将栈空间扩容至原来的2倍。但是如果我们使用缺省参数当明确知道不需要太大空间时就使用默认的空间大小当明确知道需要很大空间时就使用缺省参数。
需要注意的是默认参数只可在函数声明中出现一次如果没有函数声明只有函数定义才可以在函数定义中设定。
案例 #includeiostreamusing namespace std;struct Stack
{int* a;int top;int capacity;
};//声明
//缺省参数不能在函数声明和定义中同时出现
//默认参数只可在函数声明中出现一次如果没有函数声明只有函数定义才可以在函数定义中设定
void StackInit(struct Stack* ps, int capacity 4);int main()
{struct Stack st1;//知道我一定会插入100个数据就可以显示地传参数100这样就提前开好空间插入数据避免扩容StackInit(st1, 100);struct Stack st2;StackInit(st2);return 0;
}//定义
void StackInit(struct Stack* ps, int capacity)
{ps-a (int*)malloc(sizeof(int) * capacity);//...ps-top 0;ps-capacity capacity;
} 运行结果 五.函数重载
所谓重载(overload)函数就是在同一个作用域内函数名字相同但形参列表不同的函数。
5.1.重载函数概念
函数重载是函数的一种特殊情况C允许在同一作用域中声明几个功能类似的同名函数这 些同名函数的形参列表(参数个数或类型或类型顺序)不同常用来处理实现功能类似数据类型 不同的问题。
它们的函数名相同但参数列表却不同参数列表的不同有三种含义参数个数不同或者参数类型不同或者参数个数和类型都不同。
参数类型不同 //参数类型不同
int Add(int left, int right)
{return left right;
}double Add(double left, double right)
{return left right;
}int main()
{cout Add(1, 2) endl;cout Add(1.1, 2.2) endl;return 0;
} 运行结果 参数个数不同 //参数个数不同
void f()
{cout f() endl;
}void f(int a)
{cout f(int a): a endl;
}int main()
{f();f(1);return 0;
} 运行结果 参数类型顺序不同 //参数类型顺序不同
void func(int i, char ch)
{cout void func(int i,char ch) i ch endl;
}void func(char ch, int i)
{cout void func(char ch,int i) ch i endl;
}int main()
{func(1, a);func(a, 1);return 0;
} 运行结果 注意
1.返回值不同不能构成重载只有涉及到参数不同才会构成重载。
案例 //返回值不同不构成重载只有涉及到参数不同才会构成重载
short Add(short left, short right)
{return left right;
}int Add(short left, short right)
{return left right;
}int main()
{Add(1, 3);return 0;
} 运行结果 2.当使用具有默认参数的函数重载形式时须注意防止调用的二义性例如下面的两个函数 int add(int x, int y 1);
void add(int x); 当使用函数调用语句“add(10);”时会产生歧义因为它既可以调用第一个add()函数也可以调用第二个add()函数编译器无法确认到底要调用哪个重载函数这就产生了调用的二义性。在使用时要防止这种情况的发生。
5.2.C支持函数重载的原理--名字修饰(name Mangling)
为什么C支持函数重载而C语言不支持函数重载呢
在C/C中一个程序要运行起来需要经历以下几个阶段预处理、编译、汇编、链接。假设在Linux环境下要处理的程序为func.h func.c test.c则在每个阶段对应的执行操作分别为 预处理头文件展开宏替换条件编译去掉注释 func.i main.i编译语法检查生成汇编代码 func.s main.s汇编把汇编代码转换成二进制机器码 func.o main.o链接将.o的目标文件合并到一起其次还需要找一些只给声明的函数变量的地址合并段表符号表的合并和符号表的重定位 a.out 实际项目通常是由多个头文件和多个源文件构成而通过C语言阶段学习的编译链接我们可以知道当前test.cpp中调用了func.cpp中定义的Add函数时编译后链接前test.o的目标文件中没有Add的函数地址因为Add是在func.cpp中定义的所以Add的地址在func.o中。那么怎么办呢
所以链接阶段就是专门处理这种问题链接器看到test.o调用Add但是没有Add的地址就会到func.o的符号表中找Add的地址然后链接到一起。
那么链接时面对Add函数链接接器会使用哪个名字去找呢这里每个编译器都有自己的函数名修饰规则。
由于Windows下vs的修饰规则过于复杂而Linux下g的修饰规则简单易懂下面我们使用g演示这个修饰后的名字。
通过编译我们可以看出gcc的函数修饰后名字不变。而g的函数修饰后变成【_Z函数长度函数名类型首字母】。可以得出在Linux下采用gcc编译完成后函数名字的修饰没有发生改变而采用g编译完成后函数名字的修饰发生改变编译器将函数参数类型信息添加到修改后的名字中。 因此可以得出C语言是没办法支持重载的因为同名函数没办法区分而C是通过函数修饰规则来区分只要参数不同修饰出来的名字就不一样就支持了重载。
5.3.extern C
extern C的主要作用是为了能够正确实现C代码调用其他C语言代码。加上extern C后会指示编译器这部分代码按C语言的进行编译而不是C的。由于C支持函数重载因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中而不仅仅是函数名而C语言并不支持函数重载因此编译C语言代码的函数时不会带上函数的参数类型一般只包括函数名。
六.引用
引用不是新定义一个变量而是给已存在变量取了一个别名编译器不会为引用变量开辟内存空 间它和它引用的变量共用同一块内存空间。
6.1.引用的概念
引用就是给一个变量起一个别名用“”标识符来标识其格式如下所示 数据类型 引用名变量名; 上述格式中“”并不是取地址操作符而是起标识作用标识所定义的标识符是一个引用。引用声明完成以后相当于目标变量有两个名称如下面代码所示 int a 0;
int b a; 在上述代码中b就是变量a的引用b和a标识的是同一块内存相当于一个人有两个名字。对a与b进行操作都会更改内存中的数据。
6.2.引用的特性
a.引用在定义时必须初始化如“int b;”语句是错误的。
案例
int main()
{int a 10;int b a;int c;
}
运行结果 b.引用在初始化时只能绑定左值不能绑定常量值如“int b10;”语句是错误的
案例
int main()
{int a 10;int b a;int c 100;
}
运行结果 c.引用一旦初始化其值就不能再更改即不能再做别的变量的引用代码如下所示
int a 10;
int b 20;
int p a;
p b;//为p赋值
p为变量a的引用当pb执行时并不是把p指向了变量b而是使用变量b给变量a赋值
d.数组不能定义引用因为数组是一组数据无法定义其别名
e.一个变量可以有多个引用。
案例
int main()
{int a 0;//引用int b a;int c a;int d c;//取地址cout a endl;cout b endl;cout c endl;cout d endl;return 0;
}
运行结果 6.3.引用的使用场景
6.3.1.引用作为函数参数
C增加引用的类型主要的应用就是把它作为函数的参数以扩充函数传递数据的功能引用作函数参数时区别于值传递与地址传递。我们以交换两个数据的值为例来分析引用作为函数参数的用法。
案例
//地址传递
void Swap2(int* p1, int* p2)
{int temp *p1;*p1 *p2;*p2 temp;
}//引用传递
void Swap3(int rx, int ry)
{int temp rx;rx ry;ry temp;
}int main()
{int x 3, y 5;Swap1(x, y);cout x y endl;int m 3, n 5;Swap2(m, n);cout m n endl;int i 3, j 5;Swap3(i, j);cout i j endl;return 0;
}运行结果 分析
这是一个典型的区分值传递与址传递的函数如果是值传递由于副本机制无法实现两个数据的交换址传递则可以完成两个数据的交换但也需要为形参(指针)分配存储单元在调用时要反复使用“*指针名”且实参传递时要取地址这样很容易出现错误且程序的可读性也会下降。而引用传递就完全克服了它们的缺点使用引用就是直接操作变量简单高效可读性好。
6.3.2.引用作为函数返回值
案例1
int Add(int a, int b)
{int c a b;return c;
}int main()
{int ret Add(1, 2);cout ret endl;return 0;
}
运行结果 分析
返回普通类型对象其实是返回这个对象的拷贝c其实会创建一个临时变量这个临时变量被隐藏了它会把c的值拷贝给这个临时变量当执行语句“int ret Add(1, 2);”的时候就会把临时变量的值再拷贝给ret假设这个临时变量是t相当于做了这两个赋值的步骤t c; ret t。
函数中的普通变量是存放在当前所开辟函数的栈帧中的即存放在内存中的栈区而存放在栈区中的临时变量当函数调用结束后整个函数栈帧就会被销毁那么存放在这个栈帧中的临时变量也随之消亡不复存在。
案例2
int Add(int a, int b)
{int c a b;return c;
}int main()
{int ret Add(1, 2);cout ret endl;return 0;
}
运行结果 分析
返回引用实际返回的是一个指向返回值的隐式指针在内存中不会产生副本是直接将c拷贝给ret这样就避免产生临时变量相比返回普通类型的执行效率更高而且这个返回引用的函数也可以作为赋值运算符的左操作数。
案例3
int Add(int a, int b)
{int c a b;return c;
}int main()
{int ret Add(1, 2);Add(3, 4);cout Add(1, 2) is : ret endl;return 0;
}
运行结果 分析 在Add函数调用结束后为add函数创建的栈帧会被销毁这块栈空间会还给操作系统。此时再使用Add函数的返回值就会造成对内存空间的非法访问而大部分情况下编译器不会对非法访问内存报错。下一次的函数调用可能还是在这块空间上建立栈帧但是上一次的栈帧是否清理取决于编译器可能清理了也可能没清理
如果编译器没有清理这个栈帧的话那么这个c就还是3如果编译器清理了这个栈帧的话这个c就有可能是个随机值。
小结
引用返回的语法含义就是返回返回对象的别名使用引用返回本质是不对的因为结果是没有保障的。 出了函数作用域返回对象就销毁了那么一定不能用引用返回(使用static时可以使用引用返回)一定要用传值返回。 不要将局部变量作为返回值因为局部变量存放在栈区函数调用结束之后就释放第一次结果正确是因为编译器做了保留第二次结果错误是因为局部变量被释放了。 函数的返回值可以左值存在静态变量存放在全局区是在整个程序运行结束才释放。
引用作为返回的情况
使用static修饰的静态变量作为返回对象返回对象为调用函数中开辟的一块内存空间中的内容调用函数中开辟的空间是用malloc开辟的存放在堆上所以可以引用返回。
案例
int Count()
{static int n0;//可以使用引用//int n 0;//不可以使用引用n;//...return n;
}char func2(char* str, int i)
{return str[i];
}int main()
{//int ret Count();//ret的结果是未定义的如果栈帧结束时系统会清理栈帧并置成随机值那么这里ret的结果就是随机值int ret Count();Count() 10;//如果函数的返回值作为左值必须使用引用cout ret endl;cout ret endl;//返回一个随机值char ch[] abcdef;for (int i 0; i strlen(ch); i){func2(ch, i) 0 i;}cout ch endl; //012345return 0;
}
运行结果 总结
引用作为函数参数
输出型参数大对象传参提高效率。
引用作为函数返回值
输出型返回对象调用者可以修改返回对象减少拷贝提高效率。
6.4.传值和传引用效率比较
值和引用的作为参数的性能比较
案例
#include time.h
struct A
{ int a[10000];
};void TestFunc1(A a)
{}
void TestFunc2(A a)
{}void TestRefAndValue()
{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;
}int main()
{TestRefAndValue();return 0;
}
运行结果 值和引用的作为返回值类型的性能比较
案例
#include time.h
struct A
{ int a[10000];
};A a;//值返回
A TestFunc1()
{ return a;
}//引用返回
A TestFunc2()
{return a;
}void TestReturnByRefOrValue()
{//以值作为函数的返回值类型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 main()
{TestReturnByRefOrValue();return 0;
}
运行结果 小结
以值作为参数或者返回值类型在传参和返回期间函数不会直接传递实参或者将变量本身直接返回而是传递实参或者返回变量的一份临时的拷贝因此用值作为参数或者返回值类型效率是非常低下的尤其是当参数或者返回值类型非常大时效率就更低。
通过上述代码的比较发现值和引用在作为传参以及返回值类型上效率相差很大。
6.5.常引用
我们知道引用不能绑定常量值如果想要用常量值去初始化引用则引用必须用const来修饰这样的引用我们称之为const引用。
const引用可以用cons对象和常量值来初始化例如
const int a 10;//常量值初始化const引用
const int a 10;
const int b a;//const对象初始化const引用
一般来说对于const对象而言只能采用const引用如果没有对引用进行限定那么就可以通过引用对数据进行修改这是不允许的。但const引用不一定都得用const对象初始化还可以用非const对象来初始化例如
int a 10;
const int b a;
用非const对象初始化const引用只是不允许通过该引用修改变量值。除此之外const引用甚至可以用不同类型的变量来初始化const引用例如
double d 1.2;
const int b d;这是连指针都没有的优越性此处b引用了一个double类型的数值编译器在编译这两行代码时先把d进行了一下转换转换为int类型数据然后又赋值给了引用b其转换过程如下面代码所示
double d 1.2;
const int temp (int)d;
const int b temp;
在这种情况下b绑定的是一个临时变量。而当非const引用时如果绑定到临时变量那么可以通过引用修改临时变量的值修改一个临时变量的值是没有任何一样的因此编译器把这种行为定位非法的那么用不同类型的变量初始化一个普通引用自然也是非法的。
案例
int main()
{int a 10;int b a;cout typeid(a).name() endl;cout typeid(b).name() endl;//权限不能放大const int c 20;//int d c;//权限放大从const变为非const不合法const int d c;//权限能够缩小int e 30;const int f e;//权限缩小从非const变为const合法int ii 1;//强制类型转换并不会改变原变量类型中间会产生一个临时变量double dd ii;//ii会生成一个临时变量然后dd会拷贝这个临时变量而临时变量具有常性//double rdd ii;//会造成权限的放大ii生成的临时变量是const类型而rdd是非const类型不能从const变为非const是不合法的const double rdd ii;const int x 10;//可以为常量return 0;
}
6.6.引用和指针的区别
语法概念
引用就是一个别名没有独立空间和其引用实体共用同一块空间而指针开辟了4字节或者8字节的空间存储变量的地址。
底层实现
在底层实现上引用实际上是有空间的因为引用在底层是按照指针方式来实现的。
使用场景
指针更强大更危险更复杂而引用相对局限一些更安全更简单。
二者不同
引用概念上定义一个变量的别名指针存储一个变量地址引用在定义时必须初始化指针没有要求引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体没有NULL引用但有NULL指针在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小有多级指针但是没有多级引用访问实体方式不同指针需要显式解引用引用编译器自己处理引用比指针使用起来相对更安全。
案例
int main()
{//语法的角度ra没有开空间int a 10;int ra a;ra 20;cout a endl;//语法的角度pa没有开辟4或8字节的空间//底层实现角度引用底层是用指针实现的int b 20;int* pa b;*pa 20;cout b endl;return 0;
}
运行结果 七.内联函数
我们都直到函数的调用有利于代码重用提高效率但有时频繁的函数调用也会增加时间与空间的开销反而造成效率低下。因为调用函数实际上是将程序执行顺序从函数调用处跳转到函数所存放在内存中的某个地址将调用现场保留跳转到那个地址将函数执行执行完毕后再回到调用现场所以频繁的函数调用会带来很大开销。为了解决这个问题C提供了内联(inline)函数在编译时将函数体嵌入到调用处。
7.1.内联函数定义
以inline修饰的函数叫作内联函数编译时C编译器会在调用内联函数的地方展开没有函数调 用建立栈帧的开销内联函数提升程序运行的效率。其格式如下
inline 返回值类型 函数名(参数列表)
{函数体;
}案例
#includeiostream
using namespace std;//Add就会在调用的地方展开
inline int Add(int x, int y)
{return x y;
}int main()
{int ret Add(10, 20);cout ret endl;return 0;
}如果在上述函数前增加inline关键字会将其改成内联函数在编译期间编译器会用函数体替换函数的调用。
查看方式
在release模式下查看编译器生成的汇编代码中是否存在call Add
使用内联函数时汇编语言中不再有call Add指令函数指令直接在主函数中展开。不使用内联函数时要先通过call指令调用Add函数然后建立函数栈帧并执行函数指令。
在debug模式下需要对编译器进行设置否则不会展开(因为debug模式下编译器默认不会对代码进行优化以下给出vs2019的设置方式)。在Debug版本下内联函数展开的方法
打开属性设置选择C/C - 常规将调试信息格式改为程序数据库选择C/C - 优化将内联函数扩展改为只适用于_inline (Ob1)。
7.2.内联函数特性
a.inline是一种以空间换时间的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用。缺陷可能会使目标文件变大优势少了调用开销提高程序运行效率 b.inline对于编译器而言只是一个建议不同编译器关于inline实现机制可能不同一般建议将函数规模较小(即函数不是很长具体没有准确的说法取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性 c.inline不建议声明和定义分离分离会导致链接错误。因为inline被展开就没有函数地址了链接就会找不到。 7.3.内联函数与宏函数的区别
宏函数使用宏函数在预处理阶段进行替换 。
宏的缺点可读性差较为复杂没有类型安全检查不方便调试 宏的优点复用性变强宏函数提高效率减少栈帧建立。
C中基本不再建议使用宏尽量使用const,enum,inline去替代宏。inline几乎解决了宏函数的缺点同时兼具了它的缺点 。
八.auto关键字
C11之前auto默认修饰函数的局部变量限定变量的作用域及存储期。C11中auto称为类型说明符使用它可以让编译器根据初始化代码推断出所声明变量的真实类型。
8.1.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 d TestAuto();cout typeid(b).name() endl;cout typeid(c).name() endl;cout typeid(d).name() endl;return 0;
}
运行结果 注意
使用auto定义变量时必须对其进行初始化在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明而是一个类型声明时的“占位符”编译器在编译期会将auto替换为变量实际的类型。
8.2.auto的使用细则
auto与指针和引用结合起来使用 用auto声明指针类型时用auto和auto*没有任何区别但用auto声明引用类型时则必须加。
案例
int main()
{//用auto声明指针类型时用auto和auto*没有任何区别int x 10;auto a x;//a的类型是int*auto* b x;//显示地加*表示用于接收一个指针类型的数据cout typeid(a).name() endl;cout typeid(b).name() endl;//用auto声明引用类型时则必须加auto c x;//显示地加表示用于接收一个引用类型的数据cout typeid(c).name() endl;
}
运行结果 在同一行定义多个变量 当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错因为编译器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量。
案例
int main()
{//在同一行定义多个变量auto a 1, b 2;auto c 3, d 4.0;//该行代码会编译失败因为c和d的初始化表达式类型不同return 0;
}
运行结果 8.3.auto不能推导的场景
auto不能作为函数参数 auto不能作为形参类型因为编译器无法对其的实际类型进行推导。
案例
void func(auto x)
{cout x endl;
}int main()
{func(10);return 0;
} 运行结果 auto不能直接用来声明数组
案例
int main()
{int a[] { 1,2,3 };auto b[] { 1,2,3 };return 0;
}运行结果 注意
为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法 auto在实际中最常见的优势用法就是跟以后会讲到的C11提供的新式for循环还有lambda表达式等进行配合使用。
九.基于范围的for循环
9.1.范围for的语法
在C98中如果要遍历一个数组可以按照以下方式进行
int main()
{int a[] { 1,2,3,4,5,6 };for (int i 0; i sizeof(a) / sizeof(a[0]); i){a[i];}for (int i 0; i sizeof(a) / sizeof(a[0]); i){cout a[i] ;}cout endl;return 0;
}
对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因此C11中引入了基于范围的for循环。for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。
int main()
{int a[] { 1,2,3,4,5,6 };//范围for//自动地依次取a的数据赋值给e//自动迭代自动判断结束for (auto e : a)//加可以对数组进行更改加*不可以对数组进行修改因为无法从“int”转换为“int *”{e--;}for (auto e : a){cout e ;}cout endl;return 0;
}
注意
与普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。
9.2.范围for的使用条件
for循环迭代的范围必须是确定的 对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。
void TestFor(int array[])
{for (auto e : array)//for的范围不确定cout e endl;
}int main()
{int a[] { 1,2,3,4,5,6 };TestFor(a);}
迭代的对象要实现和的操作
十.指针空值nullptr
在良好的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()
{//NULL实际是一个宏在传统的C头文件(stddef.h)中NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量。int* p NULL;f(0);//f(int)f(NULL);//f(int)f(p);//f(int*)//C11 nullptr//在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的。//在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同f(nullptr);int* ptr nullptr;return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的初衷相悖。
在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void *)0。
注意 1. 在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的 2. 在C11中sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同 3. 为了提高代码的健壮性在后续表示指针空值时建议最好使用nullptr。