#include<stdio.h>
#include<stdlib.h>int rand=0;int main()
{printf("%d",rand);}
//报错:重定义(变量命名时与库函数名冲突)
C语言中有命名冲突:
1>变量名与库冲突(引用头文件后冲突)
2>变量名互相之间的冲突(不同模块中命名冲突)
而冲突的变量名可能在多处进行使用,修改会造成诸多不便,因此c++考虑到这一点,提出了解决办法:使用关键字namespace——命名空间,namespace可定义一个域,将变量放入域中进行隔离(解决全局变量命名冲突问题)
关于域:
#include<stdio.h>
#include<stdlib.h>int a=0;//全局域int main()
{int a=0;//局部域,出了该域后会被销毁printf("%d",a);//优先访问局部域printf("%d",::a);//::为域作用限定符
}//两个a可同时存在,因为在不同的域中
//局部域会影响生命周期也会影响访问
//作用域是域中的一种
//几种常见的域:
//类域
//命名空间域
//局部域
//全局域
#include<stdio.h>
#include<stdlib.h>int a=0;namespace hmbb1
{int a=1;namespace hmbb2{int a=3;}
}int main()
{int a=2;printf("%d",a);printf("%d",::a);printf("%d",hmbb1::a);printf("%d",hmbb1::hmbb2::a);
}//展开命名空间域
using namespace hmbb;
//访问变量时编译器的查找顺序为:局部域->全局域(即默认为局部域)
//不会主动进入命名空间中搜索,只有在展开了命名空间域or指定访问命名空间域后才会进行搜索
//但展开命名空间等于将变量变为全局变量,则需要考虑是否与原全局冲突
//命名空间中可以定义变量/函数/类型
//namespace可以互相嵌套
//多个同名的命名空间会被合并
由于全部展开命名空间有风险,可以选择展开部分,即只将常用展开
using std::cout;
using std::endl;
<< 流插入运算符
cout<<"hello world"<<x<<endl;
endl为换行
cout可自动识别类型,且可以一行连续插入多项
>>流提取运算符
由于c++兼容c语言,所以在c++中也可以用c语言,输入输出可使用scanf和printf
#include<iostream>
using namespace std;void Func(int a=0)
{cout << a << endl;
}int main()
{Func(); //没有传参时,使用参数的默认值Func(10); //传参时,使用指定的实参return 0;
}
当有多个参数时,传参遵循从左向右依次传参的规则,不可跳跃传参
必须从右往左缺省,有几个参数没有缺省则在传参时至少传几个参数
例:栈中对栈进行初始化需要动态开辟一部分空间
#include<iostream>
using namespace std;
struct Stack
{int* a;int top;int capacity;
};void StackInit(struct Stack* pst, int defaultcapacity=4)
{pst->a=(int*)malloc(sizeof(int)*defaultcapacity);if(past->a==NULL){perror("malloc fail");return;}pst->top=0;pst->capacity=defaultcapacity;
}int main()
{struct Stack st1;StackInit(&st1,100);//插入100个数据struct Stack st2;StackInit(&st2);//不知道要插入多少数据
}
为了防止声明和定义不一致,缺省参数的声明和定义不能同时缺省,只有声明时可以缺省,定义不可以
是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题
#include<iosteam>
using namespace std;
//1.参数类型不同
int Add(int left, int right)
{cout << "int Add(int left, int right)" << endl;return left + right;
}double Add(double left, double right)
{cout << "double Add(double left, double right)" <<endl;return left + right;
}//2.参数个数不同
void f()
{cout << "f()" << endl;
}void f(int a)
{cout << "f(int a)" << endl;
}//3.参数类型顺序不同
void f(int a,char b)
{cout << "f(int a, char b)" << endl;
}void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{cout << Add(1,2) << endl;cout << Add(1.1, 2.2) << endl;f();f(10);f(10,'a');f('a',10);return 0;
}
补充:返回值没有要求,比如函数名相同,参数相同,返回值不 同,那么不构成重载
//1.是否构成重载————构成
//2.问题:无参调用存在歧义
void f()
{cout << "f()" << endl;
}void f(int a =0)
{cout << "f(int a)" << endl;
}
int main()
{f(); //"f":对重载函数的调用不明确
}
为什么C语言不支持函数重载,cpp支持,cpp又是如何支持的呢?
Stack.h Stack.cpp Test.cpp
1.预处理(头文件展开、宏替换、条件编译、去掉注释......)
生成新文件Stack.i (由Stack.h和Stack.cpp生成) Test.i(由Stack.h和Test.cpp生成)
2.编译:检查语法,生成汇编代码(指令级代码)
生成新文件Stack.s (由Stack.i生成) Test.s(由Test.i生成)
3.汇编:汇编代码转换成二进制机器码
生成新文件Stack.o (由Stack.s生成) Test.o(由Test.s生成)
4.链接:生成可执行程序: / a.out(由Stack.o和Test.o生成)
调用函数会调用call指令(call + 函数名(地址)),相当于是一种跳转,然后建立函数栈帧,调用执行函数
在编译阶段即Test.i->Test.s时,由于只有声明(相当于是一个承诺),无法获得函数的地址,但是可以通过编译,而在链接时可以找到定义,本质上是兑现承诺(通过符号表来查找函数地址),如果找不到定义则报错
现在再次回到上面的问题:为什么cpp可以支持重载而C语言不可以呢
因为C语言在进行链接时,在符号表中查找函数地址是通过函数名来查找的,所以C语言不允许函数同名,如果同名则会在编译过程就报错
那么cpp为什么可以呢?
可以看出cpp查找时并不是单纯通过函数名来查找,而是使用了函数名修饰规则,根据g++的规则,_Z4是前缀,4代表函数名长度,i和id是参数类型的缩写,因此在cpp中的查找应为
所以函数名相同只要参数不同可以构成重载,可以看到返回值并没有参与在函数名修饰规则中,因此如果只有返回值不同则不构成重载。那么如果将返回值也加入函数名修饰规则中,是否能构成重载呢,也是不可以的,因为在调用函数时会出现调用歧义,无法确定调用的是哪一个函数,因此早在编译过程中就会报错。
而是不是所有的函数都需要链接呢?
并不是,编译的时候只需要有声明就可以通过,如果在编译时只有声明那么就需要链接,但是如果有定义那就可以直接拿到地址,不需要链接。
由于C语言中的指针较为复杂,因此c++采用了一种新的方式,即为引用(也就是取别名)
引用不是新定义一个变量,而是给已经存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。引用是不可以改变指向的,即引用只是取了个别名,并没有改成另一个人
比如:李逵,在家称“铁牛”,江湖人称“黑旋风”。
类型& 引用变量名(对象名)=引用实体;
注意:引用类型必须和引用实体是同种类型的
a , b , c , d共用同一块空间,++时会同时进行++
例如:原本对于两个变量进行交换的写法是用指针来完成,现在可以通过别名来完成
#include<iosteam>
using namespace std;
/*
void Swap(int* a,int* b)
{int tmp = *a;*a = *b;*b = tmp;
}int main()
{int x=0,y=1;Swap(&x,&y);cout << x << " " << y << endl;
}
*/void Swap(int& a,int& b)
{int tmp = a;a = b;b = tmp;
}int main()
{int x=0,y=1;Swap(x,y);cout << x << " " << y << endl;
}
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
#include<iostream>
using namespace std;
int main()
{//一个变量可以有多个引用int a=0;int& b=0;int& c=b;//引用在定义时必须初始化//int& d; 报错//引用一旦引用一个实体,再不能引用其他实体int x=10;c=x;//x的值赋值给c,c依旧是a/b对象别名
}
在同一个域中不可以同名引用,在不同的域中可以同名引用
1.做参数(输出型参数,即形参的改变要影响实参)
#include<iostream>
using namespace std;
typedef struct ListNode
{int val;struct ListNode* next;
}*PNode;void LTPushBack(PNode& phead, int x);int main()
{return 0;
}
2.做参数(提高效率)(大对象/深拷贝对象)
3.引用做返回值()
#include<iostream>
using namespace std;
int Count()
{static int n = 0;n++;//...return n;
}int main()
{int ret = Count();
}//因为函数被调用时会建立函数栈帧,而调用结束后函数栈帧会被销毁,因此
//传值返回会生成一个临时变量,作为这个表达式的返回值,赋值给ret(进行两次拷贝)
//但是当前程序中的n在静态区,所以不会被销毁,但是也还是会生成一个临时变量
//如果返回值所需内存较小那么该临时变量就有可能是寄存器,因为寄存器一般只有4或8个字节
如果通过引用返回则可以不用创建临时变量,减少拷贝提高效率
#include<iostream>
using namespace std;
//错误样例:不能用引用返回
int& Count()
{int n = 0;n++;//...return n;
}int main()
{int ret = Count();//ret为返回值的拷贝
}
如果函数没有使用静态变量则不能使用引用返回,因为函数调用结束后栈帧被销毁,该变量也不存在了,无法通过别名来访问得到该变量的值,相当于野指针。因此得到的返回值是不确定的,如果函数结束,栈帧销毁,没有清理栈帧,那么返回值的结果侥幸是正确的。如果函数结束,栈帧销毁,清理栈帧,那么返回值的结果是随机的。
#include<iostream>
using namespace std;
//错误样例:不能用引用返回
int& Count(int x)
{int n = 0;n++;//...return n;
}
//返回的是n的别名int main()
{int& ret = Count(10);//ret为n的别名cout << ret <<endl;// 11/随机值Count(20);cout << ret << endl;// 21/随机值
}
此种情况也不适合用引用返回
#include<iostream>
using namespace std;
//正确样例:
int& Count()
{static int n = 0;n++;//...return n;
}int main()
{int ret = Count();
}
1、基本任何场景都可以用引用传参
2、谨慎用引用做返回值。出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回。
常引用:
#include<iostream>
using namespace std;int func1()
{static int x=0;return x;
}int& func2()
{static int x=0;return x;
}int main()
{const int a=0;int& b=a;//报错//引用过程中,权限不能放大(a被const修饰,不能改变)//b的改变会影响aconst int c=0;int d=c;//正确//c赋值给d,d是c的临时拷贝,改变d不会影响c,所以没有放大权限int x=0;int& y=x;const int& z=x;++x;//可以//引用过程中,权限可以平移或者缩小//z不可以改变,但是x可以改变,但是x++后,z也会改变double dd=1.11;int ii=dd;//可以进行,发生类型转换(发生类型转换时都会产生临时变量)//此处产生int类型的临时变量int& rii=dd;//不可以//创建int临时变量,临时变量具有常性(不能被修改),所以不可以的原因在于权限的放大const int& rii=dd;//可以int& ret1=func1();//不可以//func1返回的是临时变量,具有常性,所以此处属于权限放大//改成:const int& ret1=func1(); //权限平移int ret1=func1() //拷贝int& ret2=func2(); //权限平移const int& rret2=func2(); //权限缩小}
补充:运算符两边的变量类型不同就会发生类型转换,产生临时变量
从语法层面看,引用不需要开辟空间,而指针需要
#include<iostream>
using namespace std;int main()
{int a=0;//语法层面:不开空间,是对a取别名int& ra=a;ra=20;//语法层面:开空间,存储a的地址int* pa=&a;*pa=30;
}
从底层汇编指令实现的角度看,引用是类似指针的方式实现的
1、引用概念上定义一个变量的别名,指针存储一个变量地址
2、引用在定义时必须初始化,指针没有要求
3、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4、没有NULL引用,但有NULL指针
5、在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7、有多级指针,但没有多级引用
8、访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9、引用比指针使用起来相对更安全
1、使用
#include<iostream>
using namespace std;int main()
{int a=0;int b=a;auto c=a;//根据右边的表达式自动推导c的类型auto d=1+1.11;//根据右边的表达式自动推导d的类型cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;//打印变量的类型
}
当类型很长时会使用auto
2、在用一行定义多个变量
当在用一行声明多个变量时,这些变量必须是相同的类型,否则报错
3、auto不能推导的场景
auto不能作为函数的参数
auto不能直接用来声明数组
语法糖:
#include<iostream>
using namespace std;int main()
{int a[]={1,2,3,4,5,6,7,8,9,10};//适用于数组//范围for 语法糖(访问数组)//依次取数组中数据赋值给e//自动迭代,自动判断结束for(auto e : arr){cout << e << " ";}cout << endl;//修改数据for(auto& e : arr){e *=2;}for(auto e :arr){cout << e << " ";}cout << endl;
}
1>for循环迭代的范围必须是确定的
//不可以
//当数组作为参数传入该函数,传入的是首元素的地址,是指针,而不是数组
void TestFor(int arr[])
{for(auto& e : arr)cout << e << endl;
}
2>迭代对象要实现++和==的操作
调用函数时会有一些消耗,比如建立栈帧等,C语言中可以通过宏函数来解决,但是宏函数也有一些缺点
#include<iostream>
using namespace std;//可以通过宏函数来解决,写宏函数要注意括号的使用
//#define Add(x,y) ((x)+(y))int Add(int x,int y)
{return (x+y)*10;
}int main()
{for(int i=0;i<10000;i++){cout << Add(i,i+1) << endl;}return 0;
}
//Add函数被调用10000次,每次都需要建立栈帧,则消耗很大
//宏函数
//优点:不需要建立栈帧,提高调用效率。可修改,维护性强
//缺点:复杂,容易出错,可读性差,不能调试
c++中为了更好地解决问题,使用内联函数,在调用的函数返回值之前加上关键字inline,即成为内联函数,内联函数会在函数调用的地方展开,则没有函数调用了,所以内联函数不需要建立栈帧,不复杂,不容易出错,可读性强,可以调试
#include<iostream>
using namespace std;
//内联函数
inline int Add(int x,int y)
{return (x+y)*10;
}int main()
{for(int i=0;i<10000;i++){cout << Add(i,i+1) << endl;}return 0;
}
但是宏函数和内联函数都只适用于短小的频繁调用的函数,如果用于较长的函数则会导致代码膨胀
比如:假设一个函数Func,编译后是50行指令,在执行程序时有10000个位置需要调用Func函数,
如果Func不是inline,在程序中调用函数是使用的是call Func(函数地址)的汇编指令,因此合计10000+50行指令。
如果Func是inline,那么在程序调用时会将函数在该处展开,因此合计10000*50行指令,可执行程序变大。
然而,inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器会自己决定,像类似函数就加了inline也会被否决掉:1、比较长的函数 2、递归函数 3、默认debug模式下,inline不会起作用,否则会不方便调试
注意:inline函数不能声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
#include<iostream>
using namespace std;
void f(int)
{cout << "f(int)" << endl;
}void f(int*)
{cout << "f(int*)" << endl;
}int main()
{f(0);f(NULL);
}
//函数如果不需要形参也可以不用
按照我们的想法,f(0)应该调用第一个函数,f(NULL)应该调用第二个函数,但运行结果是都调用了第一个函数,这是因为NULL其实是一个宏定义 ,NULL就为0
为了解决NULL被当成0而无法调用函数的麻烦,c++中使用nullptr来表示空指针
1、在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是c++11作为新关键字引入的
2、在c++11中,sizeof(nullptr)与sizeof((void*)0)所占字节数相同
因此,以后使用空指针最好就用nullptr
本文发布于:2024-01-28 12:21:23,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/17064156897383.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |