C++入门———补充c语言的不足

阅读: 评论:0

C++入门———补充c语言的不足

C++入门———补充c语言的不足

1.c++关键字

2.命名空间——namespace

#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;

3.c++的输入&输出

<< 流插入运算符

cout<<"hello world"<<x<<endl;

endl为换行

cout可自动识别类型,且可以一行连续插入多项

>>流提取运算符

由于c++兼容c语言,所以在c++中也可以用c语言,输入输出可使用scanf和printf

4.缺省参数(默认参数)

全缺省参数(所有参数都给了缺省值)

#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);//不知道要插入多少数据
}

为了防止声明和定义不一致,缺省参数的声明和定义不能同时缺省,只有声明时可以缺省,定义不可以

5.函数重载

是函数的一种特殊情况,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中的查找应为

 

 所以函数名相同只要参数不同可以构成重载,可以看到返回值并没有参与在函数名修饰规则中,因此如果只有返回值不同则不构成重载。那么如果将返回值也加入函数名修饰规则中,是否能构成重载呢,也是不可以的,因为在调用函数时会出现调用歧义,无法确定调用的是哪一个函数,因此早在编译过程中就会报错。

 而是不是所有的函数都需要链接呢?

并不是,编译的时候只需要有声明就可以通过,如果在编译时只有声明那么就需要链接,但是如果有定义那就可以直接拿到地址,不需要链接。

6.引用

由于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、引用比指针使用起来相对更安全

7、auto关键字

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;
}

 范围for的使用条件

1>for循环迭代的范围必须是确定的

//不可以
//当数组作为参数传入该函数,传入的是首元素的地址,是指针,而不是数组
void TestFor(int arr[])
{for(auto& e : arr)cout << e << endl;
}

2>迭代对象要实现++和==的操作

8、内联函数

调用函数时会有一些消耗,比如建立栈帧等,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被展开,就没有函数地址了,链接就会找不到

9、指针空值nullptr (c++11)

#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 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23