对C的增强
OVERVIEW
一、namespace
1.定义与使用
命名空间namespace是指标识符的各种可见范围,C++标准程序库中的所有标识符都被定义在一个名为std的命名空间中。
在C++中名称name可以是符号常量、变量、宏、函数、结构、枚举、类和对象等等。
为了避免在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突。标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。
std是c++标准命名空间,c++标准程序库中的所有标识符都被定义在std中,比如标准库中的类iostream、vector等都定义在该命名空间中,
C中的命名空间在C语言中只有一个全局作用域,C语言中所有的全局标识符共享同一个作用域标识符之间可能发生冲突
C++中的命名空间命名空间将全局作用域分成不同的部分,不同命名空间中的标识符可以同名而不会发生冲突,命名空间可以相互嵌套。全局作用域也叫默认命名空间
-
Point1:
<iostream>
与<iostream.h>
是不一样的二者是两个文件,里面包含的代码是不一样的。- 后缀为.h的头文件c++标准已经明确提出不支持了,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里,
- c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h。 因此,
- 当使用
<iostream.h>
时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现; - 当使用
<iostream>
的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
-
Point2:C++命名空间的定义:
namespace name { ...... }
-
Point3: 由namespace的概念使用C++标准程序库的任何标识符时,可以有三种选择:
-
直接指定标识符
std::cout << std::hex << 3.4 << std::endl;
-
使用using关键字
using std::cin; using std::cout; using std::endl; cout << std::hex << 3.4 << endl;
-
最方便的就是使用using namespace std:
这样命名空间std内定义的所有标识符都有效。就好像它们被声明为全局变量一样。那么以上语句可以如下写:
cout <<hex << 3.4 << endl;
因为标准库非常的庞大,所以程序员在选择的类的名称或函数名 时就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。
但这又会带来了一个新问 题。无数原有的C++代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。所以就有了<iostream.h> 和
<iostream>
等等这样的头文件,一个是为了兼容以前的C++代码,一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加.h
-
2.实际案例
#include <iostream>
using namespace std;
namespace spaceA {
int g_a = 10;
}
namespace spaceB {
int a = 20;
namespace spacec {
struct teacher {
int id;
char name[64];
};
}
}
int main() {
cout << spaceA::g_a << endl;
spaceB::spacec::teacher t1;
return 0;
}
二、const
1.const修饰指针
#include <iostream>
int main(void) {
//1.常整形数
const int a;
int const b;
//2.c是⼀个指向常整形数的指针(所指向的内存数据不能被修改,但是本⾝可以修改)
const int* c;
//3.d常指针(指针变量不能被修改,但是它所指向内存空间可以被修改)
int* const d;
//4.e⼀个指向常整形的常指针(指针和它所指向的内存空间,均不能被修改)
const int* const e;
return 0;
}
int setTeacher_err( const Teacher *p) :Const修改形参的时候,在利用形参不能修改指针所向的内存空间
合理的利用const的好处,
- 指针做函数参数,可以有效的提高代码可读性,减少bug;
- 清楚的分清参数的输入和输出特性
2.const在CC++中的不同
C与C++中const的区别?
- 在C语言中会分配存储空间,而在C++中编译器不会分配存储空间,所以在C编译器中可以通过指针来改变用const修饰的变量的值,C++不能。
- 在C++中const修饰的变量,在编译的时候如果前面有 extern 和取地址符 & 时,会为变量分配存储空间是为了兼容C语言。
- 但是在C++中用const修饰的变量,就真的无法修改它的值,可以说是常亮。
在C语言中的const:
- const修饰的变量是只读,本质上还是一个变量,不是真正的常量,const修饰的变量不是常量而是只读变量!
- const修饰的局部变量在栈上分配存储空间
- const修饰的全局变量在只读存储区中分配存储空间
- const在只在编译期有用,在运行的时候没有用
#include<stdio.h>
int main() {
const int a = 10; printf("%d\n",a);
int *p = (int *)&a; *p = 20;//利用指针来修改const变量的值
printf("%d\n",a);
return 0;
}
在C++语言中的const:
- const修饰的变量是常量放在符号表中( 变量没有地址和分配的空间),在编译的过程中,如果发现常量则直接以符号表中的值进行替换。
- 在编译的过程中,如果发现以下情况,则为常量分配存储空间:
- 对const常量使用了 extern(当const常量为全局,并且需要在其它文件中使用,会分配存储空间)
- 对const常量使用了取地址符&(当使用&操作符,取const常量的地址时,会分配存储空间 )
- 当const int &a = 10; const修饰引用时,也会分配存储空间
- 在这两种情况下,虽然会为常量分配存储空间,但是却不会使用该存储空间中的值
#include <iostream>
using namespace std;
int main() {
const int a = 10; cout << a << endl;
int* p = (int*)&a; *p = 20;//a在符号变中并没有分配空间 没有地址
cout << a << endl;
cout << *p << endl;//修改的是一个新的地址空间
int array[a] = {0};
return 0;
}
注:如果对一个常量取地址,编译器会临时开辟一个空间,让这个指针存放这个临时空间的地址。
3.const与define
宏定义:
- 在预处理阶段将变量进行替换,
- 宏定义由预处理器处理单纯的文本替换(变量替换无作用域限制)
C++中的const:
- 是一个真正的常量,而不是C中只读变量,
- 在编译阶段将变量进行替换,
- 由编译器处理提供类型检查和作用域检查(变量替换有作用域限制)
三、其他增强
1.三目运算符
- C语言返回变量的值 C++语言是返回变量本身,C语言中的三目运算符返回的是变量值,不能作为左值使用
- C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方(三目运算符可能返回的值中如果有一个是常量值,则不能作为左值)
- C语言如何支持类似C++的特性呢? 当左值的条件:要有内存空间;C++编译器帮助程序员取了一个地址而已
2.枚举类型
- c 语言中枚举本质就是整型,枚举变量可以用任意整型赋值,
- 而 c++中枚举变量, 只能用被枚举出来的元素初始化,
#include <iostream>
using namespace std;
enum season {
SPR,
SUM,
AUT,
WIN
};
int main() {
enum season s = SPR;
//s = 0;//error但是C语⾔可以通过
s = SUM;
cout << "s = " << s << endl;//1
return 0;
}
3.全局变量重定义
- 在C语⾔中,重复定义多个同名的全局变量是合法的,多个同名的全局变量最终会被链接到全局数据区的同⼀个地址空间上
- 在C++中,不允许定义多个同名的全局变量(C++直接拒绝这种⼆义性的做法)
int g_var;//bss段
int g_var = 1;//data段
对C的扩展
一、内联函数
c 语言中有宏函数的概念。宏函数的特点是内嵌到调用代码中去,避免了函数调用 的开销。
但是由于宏函数的处理发生在预处理阶段,缺失了语法检测和有可能带来的语 意差错。
1.基本概念
C++提供了 inline 关键字,实现了真正的内嵌。
函数调用过程中有压栈与出栈的过程,将参数的环境压入栈中、将函数参数压入栈中、将实参的值拷贝到形参中、函数返回值出栈、开销比较大,
优点:避免调用时的额外开销(入栈与出栈操作)
代价:由于内联函数的函数体在代码段中会出现多个副本,因此会增加代码段的空间。
本质:以牺牲代码段空间为代价,提高程序的运行时间的效率。
适用场景:函数体很小,且被频繁调用。
#include <iostream>
using namespace std;
inline void func(int a) {
a = 20;
cout << a << endl;
}
int main(void) {
func(10);
// 编译器将内联函数的函数体直接展开
// {
// a = 20;
// cout << a << endl;
// }
return 0;
}
- 内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。
- 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)。
- 内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等)
- 内联函数由 编译器处理,直接将编译后的函数体插入调用的地方,
- 宏代码片段 由预处理器处理, 进行简单的文本替换,没有任何编译过程。
- C++中内联编译的限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 不能对函数进行取址操作
- 函数内联声明必须在调用语句之前
- 编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。
2.内联函数与宏函数
宏函数:
- 优点:内嵌代码,辟免压栈与出栈的开销
- 缺点:代码替换,易使⽣成代码体积变⼤,易产⽣逻辑错误。
内联函数:
- 优点:⾼度抽象,避免重复开发
- 缺点:压栈与出栈,带来开销
#include <iostream>
#include <string.h>
using namespace std;
#define SQR(x) ((x) * (x))
inline int sqr(int x) {
return x * x;
}
int main() {
int i = 0;
while (i < 5) {
//printf("%d\n",SQR(i++));
printf("%d\n", sqr(i++));
}
return 0;
}
二、默认参数与占位参数
通常情况下,函数在调用时形参从实参那里取得值。
对于多次调用函数同个实参时C++给出了更简单的处理办法。给形参以默认值,这样就不用从实参那里取值了。
1.单个默认参数
函数的默认参数,如果传入数据就用自己的数据,否则使用默认值
void myPrint(int x = 3) {
cout << "x: “ << x << endl;
}
2.多个默认参数
- 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值(该位置向后都得有)
- 如果函数声明有默认参数,则函数实现就不能有默认参数(声明和实现只能有一个有默认参数)
#include<iostream>
using namespace std;
int func(int a, int b = 20, int c = 30) {
return a + b + c;
}
int main() {
cout << func(10, 100) << endl;
system("pause");
return 0;
}
//10 + 100 + 30 = 140
3.占位参数
C++中函数的形参列表里可以有占位参数用来做占位,调用函数时必须填补该位置:
函数占位参数,占位参数只有参数类型声明,⽽没有参数名声明,⼀般情况下在函数体内部⽆法使⽤占位参数。
#include <iostream>
int func(int a, int b, int) {
return a + b;
}
int main() {
func(1, 2); //error 必须把最后⼀个占位参数补上
printf("func(1, 2, 3) = %d\n", func(1, 2, 3));
return 0;
}
可以将占位参数与默认参数结合起来使⽤、意义为以后程序的扩展留下线索,并兼容C语⾔程序中可能出现的不规范写法
//C++可以声明占位符参数,占位符参数⼀般⽤于程序扩展和对C代码的兼容
#include <iostream>
int func2(int a, int b, int = 0) {
return a + b;
}
int main() {
// 如果默认参数和占位参数在⼀起,都能调⽤起来
func2(1, 2);
func2(1, 2, 3);
return 0;
}
三、函数重载🚩
1.函数重载情况
作用:函数名可以相同,从而提高复用性
函数重载满足的条件:
- 函数都在同一个作用域下(这里为全局作用域)
- 函数名称相同
- 函数参数类型不同 or 参数个数不同 or 参数顺序不同
- 函数的返回值不可以作为函数重载的条件。
- 如果没有完全的类型匹配,则进行类型隐式转换,若都没有匹配到则调用失败。
#include<iostream>
using namespace std;
void func() {
cout << "func 函数的调用" << endl;
}
void func(int a) {
cout << "func(int a) 函数的调用" << endl;
}
void func(double a) {
cout << "func(double a) 函数的调用" << endl;
}
int main() {
func();
func(10);
func(3.14);
return 0;
}
2.引用作为函数的重载条件
- 引用作为重载的条件(参数类型不同的函数重载)
- 引用必须在一个合法的内存空间(栈区or堆区),常量10位于常量区不是合法引用
#include<iostream>
using namespace std;
//函数重载注意事项
//引用作为重载的条件(参数类型不同的函数重载)
void func(int &a) {
//int &a = 10;引用必须在一个合法的内存空间(栈区or堆区),10位于常量区不合法引用
cout << "func(int &a)函数的调用" << endl;
}
void func(const int &a) {
//const int &a = 10;编译器自动进行了优化:int temp = 10; const int &a = temp;合法引用
cout << "func(const int &a)函数的调用" << endl;
}
int main() {
int a = 10;
func(a);//由于a为一个变量,为可读可写的状态,故传入参数后应该调用可读可写的函数func(int &a)
func(10);//由于10位一个常量,故传入参数后应该调用函数func(const int &a)
return 0;
}
3.函数重载遇到函数的默认参数
一个函数不能即作为重载,又作为默认参数,当少写一个参数时系统无法确认是重载还是默认参数,
当函数重载碰到默认参数,会出现二义性报错(应应尽量避免这种情况)
#include<iostream>
using namespace std;
//函数重载注意事项
//函数重载碰到默认参数
void func2(int a) {
cout << "func2(int a)函数的调用" << endl;
}
void func2(int a, int b = 10) {
cout << "func2(int a, int b = 10)函数的调用" << endl;
}
int main() {
//当函数重载碰到默认参数,会出现二义性报错(应尽量避免这种情况)
//无法确定调用哪个函数
func2(10);
return 0;
}
4.函数重载与函数指针
函数指针基本语法:
//函数指针定义
//方法1 声明函数类型
typedef int(myfunc)(int, int);
//定义函数指针
// myfunc *fp = nullptr;
// fp = func;
myfunc(10, 20);
//方法2 声明函数指针类型
typedef int(*myfunc_p)(int a, int b);
//定义函数指针
// myfunc_p fp = nullptr;
// fp = func;
myfunc(10, 20);
//方法3 定义函数指针 变量
int (*myfunc_v)(int a, int b);
// myfunc_v = func;
myfunc(10, 20);
函数重载和函数指针结合:
- 函数重载与函数指针
- 当使⽤重载函数名对函数指针进⾏赋值时
- 根据重载规则挑选与函数指针参数列表⼀致的候选者
- 严格匹配候选者的函数类型与函数指针的函数类型
#include <iostream>
using namespace std;
int func(int x) {
return x;
}
int func(int a, int b) {
return a + b;
}
//在函数指针类型声明时就发生了函数重载
typedef int (*PFUNC1)(int a); //int(*)(int a)
typedef int (*PFUNC2)(int a, int b); //int(*)(int a, int b)
int main() {
int c = 0;
PFUNC1 pfunc1 = func;
c = pfunc1(1);
printf("c = %d\n", c);
PFUNC2 pfunc2 = func;
c = pfunc2(1, 2);
printf("c = %d\n", c);
return 0;
}
四、引用&
1.基本概念
变量名实质上是一段连续存储空间的别名,通过变量的名字使用存储空间
引用实际上就是给变量起别名,语法:数据类型 &别名 = 原名
-
引用没有定义,只是一种关系型声明
-
引用必须进行初始化(不允许出现
int &b;
) -
引用在初始化之后,便不可以再改变(
c = b;
赋值操作,不是在更改引用) -
本质:在C++内部实现的一个指针常量(一旦初始化后就不可发生改变),
C++编译器在编程的过程中使用常指针作为引用内部的实现,因此所占用的空间大小与指针相同。引用会让人误会其只是一个别名、没有自己的存储空间,这是C++为了实用性而做出的细节隐藏。
#include <iostream>
using namespace std;
int main() {
int a, b;
int &ref = a; //正确
int &ref = b; //错误 不可更改原有的引用关系 const不可修改
float &reff = b; //错误 引用类型不匹配
int &refff = ref; //正确 可对应用再次引用 表示为变量a起两个别名 ref和refff
}
#include <iostream>
using namespace std;
//int* const ref = &a;同理
void changeValue(int& ref) {
//*ref = 100;同理
ref = 100;
}
int main() {
int a = 20;
//1.取地址
int *p = &a;
*p = 30;
cout << "a = " << a << endl;
//2.引用 int&是引用数据类型 ref为a的别名
//int* const ref = &a;内部自动转换
int& ref = a;
//*ref = 20;内部自动转换
ref = 40;
cout << "a = " << a << endl;
//3.引用做函数参数
changeValue(a);
cout << "a = " << a << endl;
return 0;
}
2.引用做函数参数
作用:函数传递参数时,可以利用引用技术让形参修饰实参,可以简化指针修改实参,
- 相较于值传递,引用传递没有值拷贝的过程,更节省程序运行开销
- 相较于地址传递,引用传递的语法更清楚简单
#include<iostream>
using namespace std;
//1.值传递
void mySwap01(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//2.地址传递(用指针接收地址)
void mySwap02(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
//3.引用传递()
int mySwap03(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int main(){
int a = 10;
int b = 20;
//mySwap01(a, b);//值传递,形参不会修改实参
//mySwap02(&a, &b);//地址传递,形参会修改实参
mySwap03(a, b);//引用传递,形参会修改实参
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
3.引用做函数返回值
当函数返回值为引用时:
- 若返回栈变量,不能成为其他引用的初始值(不能作为左值使用)不要返回局部变量引用
- 若返回静态变量或全局变量,可以成为其他引用的初始值(可以作为右值使用,也可作为左值使用)
当引用作为函数返回值:
- 如果返回值为引用,则可以当左值
- 如果返回值为普通变量,则不可以当左值
#include<iostream>
using namespace std;
//1.不要返回局部变量的引用
int& test01() {
int a = 10;//局部变量存放在栈中
return a;
}//int &temp = a;
//2.函数的调用可以作为左值
int& test02() {
static int a = 10;//静态变量存放在全局区,全局区上的数据在程序结束后由系统释放
return a;
}
int main(){
//1.不要返回局部变量的引用
int &ref = test01();//在test01中将a的别名进行返回,在main函数中使用ref接住
//cout << "ref = " << ref << endl;//第1次结果可能会正确,是因为编译器做了保留
//cout << "ref = " << ref << endl;//第2次结果错误,因为a的内存已经释放(非法操作)
int &ref2 = test02();
cout << "ref2 = " << ref2 << endl;//第1次结果可能会正确,是因为编译器做了保留
cout << "ref2 = " << ref2 << endl;//第2次结果也是正确,只有在整个程序执行完后才会被释放
//2.函数的调用可以作为左值 如果函数的返回值是引用,那么这个函数调用可以作为左值
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
system("pause");
return 0;
}
4.const常量引用
- 作用:常量引用主要用来修饰形参,防止误操作,在形参列表中可以使用const修饰形参,防止形参改变实参
- 语法:
const 数据类型 &别名
- 常量引用:
const int &e
==const int* const e
- 普通引用:
int* const e
- 当使用常量对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名。
- 使用常量对const引用初始化后,将生成一个只读变量
#include<iostream>
using namespace std;
//int* const ref = &a;
void showValue(const int &val) {
cout << "val = " << val << endl;
}
int main() {
// 常量引用使用场景:修饰形参,防止误操作
// int a = 10;
// int& ref = a;//1.引用必须应用一块合法的内存空间(栈区or堆区),常量10在常量区(int& ref = 10;为非法)
// const int& ref = 10;//2.加上const之后 编译器可自动将代码修改为int temp = 10; const int& ref = temp;(自动分配内存空间)
int a = 100;
showValue(a);
cout << "a = " << a << endl;
return 0;
}