目录
c语言对于名字冲突的问题,无法解决,所以在c++中有了命名空间,用来解决命名冲突的问题,事实上很多库里自己就有定义一个函数名,我们如果在源程序的全局作用域里定义了相同的名字(可能是函数,也可能是作为一个变量名),这时候,就会出现问题。
假如我们引用了c的rand函数头文件,但自己又定义了一个rand变量,这时就会冲突
1.1命名空间定义
namespace zsl { int rm = 1; int vb(char a) { //..... } struct Node { int a; }; } 可以定义变量、函数、类型
namespace zsl { int rm = 1; int vb(char a) { //..... } struct Node { int a; }; namespace zsl111 { int c = 2; } } 可以嵌套定义
注意,一个工程文件里,可能有头文件、源文件,在相同工程文件的不同文件里,如果我们定义了相同名称的命名空间,最后编译器会将其合并到同一个命名空间
关于这点,假如我们手搓了一个栈,但是为了防止在实际写项目时出现冲突(比如别人也定义了一个栈,那栈的操作函数就可能引用错误了栈的声明),我们可以在头文件和(定义栈操作函数)源文件里各用相同的命名空间包含它们,这样就会被编译器放在一起。
一个命名空间,就定义了新的作用域,命名空间的所有内容都被限制于命名空间
1.2命名空间引用
#include<stdio.h> namespace zsl { int rm = 1; int vb(char a) { //..... } struct Node { int a; }; namespace zsl111 { int c = 2; } } int main() { printf("%d", zsl::rm); zsl::vb('a'); struct zsl::Node b; printf("%d", zsl::zsl111::c); return 0; } ::是作用域限定符
#include<stdio.h> namespace zsl { int rm = 1; int vb(char a) { //..... } struct Node { int a; }; namespace zsl111 { int c = 2; } } using zsl::rm; int main() { printf("%d", rm); zsl::vb('a'); struct zsl::Node b; printf("%d", zsl::zsl111::c); return 0; } using命名空间的某个成员,就不用额外加域限定符,默认引用这块空间的内容
#include<stdio.h> namespace zsl { int rm = 1; int vb(char a) { //..... } struct Node { int a; }; namespace zsl111 { int c = 2; } } using namespace zsl; int main() { printf("%d", rm); vb('a'); struct Node b; printf("%d", zsl111::c); return 0; } 这样就不用指定zsl了
?如果同时展开了两个命名空间,且有相同的变量,那么调用变量时,会按语句顺序,从先展开的命名空间里找。
#include<iostream> using namespace std; int main() { cout << "sss"<<endl; return 0; } std是c++官方库定义的命名空间,要用c++的库,基本都要用到std 里的内容 工程时尽量不要直接展开,因为容易冲突,可以指定展开 平时自己写代码可以直接展开,方便写代码 cout是标准输出对象(控制台),cin是标准输入对象(键盘) 使用时要使用命名空间std,还要包含头文件<iostream> endl是换行符,也在<iostream> <<是流插入运算符,>>是流提取运算符 相比c,cin和cout都是自动识别类型的。
要注意的是,我们引用c++的头文件,都不用加.h后缀,是因为,c++是在c的基础上诞生的,为了跟原来的c语言库文件区分,除了多了命名空间外,引用c++头文件外不用加.h后缀
#include<iostream> using namespace std; int test(int a = 0,int b=0,int c) { cout << a<<b<<c; } 传参时,必须从左往右给,不能隔着给 test(1,2,3); test(1,2); test(1); test(); int main() { test();//这里没有传参,那么函数里,a就是0 test(10,20,30);//这里传参了,那么函数里,a就是传的值 return 0; } 缺省参数也分全缺省和半缺省 上面的是全缺省 下面是半缺省 int test(int a ,int b=0,int c=1) { cout << a<<b<<c; } 注意,半缺省,只能从右往左给,必须连续 test(1); test(1,2); test(1,2,3); 注意,如果在声明函数和定义函数时都给了缺省参数,那么编译器就不知道究竟是哪个了 也不能只在定义位置给,因为如果别人调用了头文件的声明部分呢 所以最后只能在声明的地方给缺省参数
函数重载类似解决重名问题,但重点是在同一作用域里,我们前面采用命名空间,是可以引用到不同的作用域里的函数、变量。
但如果在同一个作用域,那么重名的函数怎么办呢,c语言不支持同一作用域里的重名函数,
c++就支持了重名函数(参数不同、参数个数不同、参数顺序不同之一),返回值没有要求,不同返回值还是代表同样的函数,因为在传参时,编译器是对参数进行匹配,从而确定是哪个函数的
int as(int a, int b, int c) { return 1; } int as(int a, int b) { return 1; } int as(int a, int b, double c) { return 1; } int as(double a, double b, double c) { return 1; } int as(double c,int a, int b) { return 1; } int main() { as(2, 3, 4); as(2, 3); as(1, 2, 3.1); as(2.1, 1.1, 3.1); as(2.1,3, 3); return 0; } 注意缺省参数与没有缺省参数,也构成了重载 参数重载还有很多类型,我们可以私下尝试下,结合报错信息 就可以看到很多种不同类型的函数重载
4.1编译器原理(部分)
我们先简单梳理下,以便待会解释,为什么c语言不支持重载而c++重载
编译器的还有一些内容,我在c语言的文章里有写。
假如此时:func.h(包含了函数的声明),func.c(包含了函数的定义和.h文件的引用),test.c(包含了调用函数和.h文件的引用)文件
接下来,当我们启动程序,编译器先进行预处理,生成func.i(包含了函数声明和定义),test.i(函数的声明和实际调用),
接下来,我们进行编译,此时,编译器并没有找到函数的真实地址,只是通过声明,暂时放了个调用在上面。
再接下来是汇编,将代码变成二进制代码,最后通过链接,链接整个工程文件,此时因为func.c文件有函数的定义,最终编译器可以成功将暂时放的调用代码替换成真实的函数地址,完成编译。
接下来解释c语言不支持重载的原因,其实就是c语言调用函数,是通过函数名找地址,但c++中,找函数时,是将函数的参数也放进去,这样就能更加精确找到函数。
?引用就是给变量和函数起别名
int a = 1; int& b = a; int& c = a; int& d = c; cout << a; cout << b; cout << c; cout << d; (跟define和typedef不一样) 因为,define是简单的文字替换 typedef重新定义类型 而引用是创建一个引用变量,在上面的例子里面 可以看见,a、b、c、d都是共用一个空间,可以理解为一个 内存空间,有多个名称,因此这几个变量的地址都是相同的 一个变量可以有多个引用,引用变量和引用实体都必须是同一个类型 引用变量必须初始化 引用一旦引用了一个实体,不能再引用别的实体 如果引用实体是个常量 要加const const int&a=3; 本质上,常引用的宗旨是,权限不能放大,但可以放小 const int a=1; int &b=a//这是权限放大,错误 int a=1; const int&b=a;//权限缩小,可以 int i=1; double b=i;//赋值操作 //这里会进行隐式转换类型 //所以会创建一个临时变量 double &rb=i//错误,因为赋值语句,是会创建临时变量 //比如上面的上面,i的值会赋给临时变量,临时变量的值再拷贝给b //而这里也是一样,但问题是,临时变量本质上是常量,或者是临时常量 //所以,可以这样写 const double&rb=i//这样就可以了
5..1用途
5.1.1用途1:传参
void Swap(int& l, int& r) { int tmp = l; l = r; r = tmp; } int main() { int a = 1; int& b = a; int& c = a; int& d = c; cout << a; cout << b; cout << c; cout << d; int h = 2; int& j = h; Swap(a, j); cout << a; return 0; } 乍一看好像跟指针区别不大, 但参考我c语言版链表文章中,单链表插入时,我们需要使用 2级指针,但有了这个,我们就可以直接用引用写
注意,c++的引用不能二次改变指向的引用实体,这样使得指针不能被抛弃,其他语言如java的引用是可以二次改变指向引用实体。
5.1.2返回值
首先,我们理解这块代码, int add(int a, int b) { a++; return a; } 这个函数就是传统的传值返回,函数 栈在结束函数调用后销毁,销毁前把a的值 拷贝到寄存器或其他一块空间,最后再把值拷贝回下面的c int& add2(int a, int b) { a++; return a; } 这是传引用,但事实上,这里的代码是很危险的 因为,传引用,意味着,是将a的别名,返回给b ,本质上还是把a所处的空间的返回了,而a是局部变量,函数调用结束 这块空间名义上是非法的,如果期间有别的操作,很可能使这块空间 被覆盖,使得值变成不确定的值。 int main() { int c = add(3, 4); int b = add2(3, 4); int&d = add2(3, 4); cout << c << endl << b; return 0; } 注意,引用返回,针对的是在函数调用后不会销毁的变量等。 比如静态变量,全局变量
引用返回和传值返回,效率差距比较大,尤其是返回的数据很大时,传值返回时,要额外一个临时变量来存储返回值,再把临时变量的值拷贝给函数调用时赋值的对象。
5.2引用和指针
在语法上,引用是不开辟额外空间的,指针开辟额外空间,底层上,两者都是开辟额外空间。但一般,我们默认把引用认为是不开辟额外空间的,都是把引用认为是别名。
1.引用概念上就是定义一个变量的别名,指针存储一个指针地址
2.引用必须初始化,指针没有要求
3.引用不可以二次改变指向的变量,指针可以任意改变
4.没有NULL引用,但有NULL指针
5.sizeof针对引用,计算的是引用的引用实体大小,计算指针是计算指针变量本身占据空间的大小(比如32位的4字节)
6.引用自加,引用实体也会+1,指针自加,只会按当前指针指向的类型,后跳相应字节数。
7.有多级指针,但没有多级引用
8.引用调用时,不需要额外加解引用,编译器自己会处理,指针要自己加解引用
9.引用相对指针比较安全
?c语言有宏函数,可以通过替换文本的方式,设计计算,但对复杂计算很容易出事,比如优先级问题,类型安全问题等,所以c++加了内联函数,通过inline关键字,可以让函数调用变成变成函数展开,同样可以减少开辟函数空间的行为,并且可以克服类型安全、优先级等问题。而且内联函数可以调试。
inline int add(int a, int b) { a++; return a; } int main() { int c = add(3, 4); return 0; } 注意,内联函数本质也是展开,那么如果函数的代码量很大, 并且调用次数多,那么整个程序的代码量就会飞升 inline对编译器来说,只是一个建议,编译器可以选择忽略inline特性 一般对函数规模小的,编译器会受理,对递归、规模大的函数,编译器可以选择忽略
注意,内联函数没有地址,因为内联函数会被展开,这时候,编译器不会寻找它的地址,那么如果声明和定义在两个文件,那么最后整个程序运行时,编译器就会报错,因为在调用函数的位置上,被展开的是声明,但声明本身不能被当做函数使用,需要定义才可以,所以才会报错。
因此通常声明和定义不分离对于内联函数。
?auto是用来简化很长的类型名,具体可以看我后面关于stl等方面的文章。
auto本质上让编译器通过赋值的内容,自动推到变量的类型。
int a = 1; auto c = 1; //auto j;是错误的,因为没有赋值,编译器不能推导类型,而上面的 //的表达式因为有赋值,就可以推导类型。 auto* c = &a; auto d = &a; //auto*和auto没有区别,类型都会被识别为地址 auto& b = a; //如果要用引用,必须要&符号,因为不加,编译器会认为是一般的情况,而不是引用 auto g = 3, h = 1; //同行多个变量,必须保证类型相同,编译器会根据第一个值的类型,给后面的 //变量替换类型。 auto不能作为形参的类型,也不能作为返回值。 auto也不能用来定义数组
?
int h[3] = { 0,2,3 }; for (int e : h) { cout << e; } for (int& e : h) { e *= 3; } 范围for,不需要我们自己计算数组究竟有多大,会自动判断范围 自动从下标低到高。 第二个for是通过引用的方式,改变数组自身的值
注意,范围for必须有明确的数组范围
int ao(int h[]) { for (int e : h) { //,... } } 这样就是错误写法,因为不明确范围。 迭代的对象要能++和==,具体之后的文章里我会再说
?在c的头文件中,关于NULL,是定义成0常量或者(void*)0,但编译器一般都是识别为0常量,但由于0在地址里也是空,所以用NULL初始化指针也不会出错,但如果遇到了要分开的时候。
int ao(int a) { } int ao(int* a) { } int main() { ao(0); ao(NULL); return 0; } 对于这两个函数的调用,编译器在识别函数的时候,因为默认是常量0 所以,这两次调用,实际上调用的都是形参是int a的函数 所以c++11里面引入了新的关键字nullptr nullptr等同于(void*)0,所占空间大小也是一样的