c++基于c的修改,在写c代码的时候有时会遇到下面的一种情况
#include<stdio.h>
#include<stdlib.h>
int rand = 0;
int main()
{
printf("%d", rand);
return 0;
}
代码的意思很简单,定义了一个全局变量rang,并在主函数中打印rand的值,但是这个时候运行是会报错误的。
命名空间的使用
这个错误的原因就是在stdlib.h中定义了一个函数这个函数的名字也就是rand,而我又定义了一个全局变量rand,这就造成了错误,为了解决这种问题,c++就引用了命名空间。例如下面这样,
#include<stdio.h>
#include<stdlib.h>
namespace H
{
int rand = 0;
}//命名空间的名字是由我们自己决定的,这里我就在H的命名空间中定义了一个rand
//如果我要在主函数中使用这个变量只用使用::这个符号就可以,
int main()
{
printf("%p", rand);
printf("%d", H::rand);
//这样写就不会报错了。
return 0;
}
除此之外在命名空间中能够定义的东西,不止有变量,还可以定义结构体,函数等等。
而命名空间的使用也是为了解决c中命名冲突的问题。在c中遇到这种情况解决方法通常只能是修改全局变量的名字,而c++就中就能够使用命名空间去解决这种问题。
命名空间的展开
那如果在命名空间中定义了一个变量,而且需要经常使用,在这种情况下就可以使用using展开命名空间。
命名空间的展开也分为两种一种为部分展开命名空间一种为完全展开命名空间。
例如下面
#include<stdio.h>
namespace k
{
int h = 30;
struct people
{
int age = 18;
};
}//在这个命名空间中我定义了一个名叫people的结构体
using k::people;//这就是部分展开,那么在使用people去定义新变量时就不需要使用::
//但是对于h仍旧要使用::去访问,
using namespace k;//这就是完全展开命名空间,如果这样写了,那么对于h也不需要使用::去访问了
//但是如果这个时候你又定义了一个全局变量h那么又会报错,原因和上面报错的原因是一样的。
int main()
{
//然后在下面我需要多次使用这个结构体去创建多个变量那么每次都要使用::就会显得很麻烦,那就可以将k这个命名空间给展开
//如果选择的是全部展开,那么命名空间中的所有变量都可以不使用::
k::people lisi;
k::people zhangsan;
people kai;
printf("%d\n", lisi.age);
printf("%d\n", zhangsan.age);
printf("%d\n", kai.age);
return 0;
}
c++输入和输出的使用
在c++中输入使用cin,输出使用的是cout。和c不同的是c++的cin和cout能够自动识别类型例如下面这样
除此之外还有对于命名空间是全展开还是部分展开这里网上也有一些建议
下面是我找到的一些建议
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
- 在日常练习中,建议直接using namespace std即可,这样就很方便。
- using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对 象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模 大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
#include<iostream>
using std::cin;
using std::cout;
using std::endl;//endl为换行符作用与\n一样也是存在与std中的
//因为cin和cout是在命名空间std中的这里我就只将std部分展开了。
int main()
{
int t;
double s;
char c;
cin >> t >> s >> c;//并且输入也可以直接一并输入
cout << t<<endl << s<<endl <<c <<endl;
return 0;
}
而实现这一功能靠的也就是函数重载,那如果想要改变浮点数的精度又要怎么做呢?这里我的建议是配合c语言一起使用,虽然c++也可以改变浮点数的精度,但是需要调用函数去解决。
缺省参数
在c中写了一个函数如果该函数需要两个参数,但是如果使用时只传入一个参数那就会报错,但是c++使用了缺省参数的概念,能够让一些使用两个参数的函数即使只传入一个参数也能够使用。
例如下面的这些情况
#include<iostream>
using namespace std;//完全展开std命名空间
int add(int a = 0, int b = 0)
{
return a + b;
}
int main()
{
//很明显这个函数需要两个参数如果使用的是c,那么必须传入两个参数才能正常的运行代码,
//但是在c++中可以使用缺省参数来达到只闯入一个或两个形参就能够正常运行
cout<<add(1, 2)<<endl;//传入两个参数
cout << add(1) << endl;//只传入个参数,并且实参输入默认为从左到右,即使在的前面加一个,也不能将1传给第二个参数,只能从左往右输入
cout << add() << endl;//这里就直接使用默认的实参
return 0;
}
函数重载
以两数相加函数为例,在c中如果你要写一个整型和浮点型的相加函数,那么除了写两个函数以外没有其它的方法,但是在c++中则可以使用函数重载解决问题。
c++中对于函数重载较为官方的说法:
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
而函数重载要满足的要求有几个:
1.传入的参数类型不同
2.传入的参数数量不同
3.传入的参数类型顺序不同(记住是类型顺序)
下面是例子:
#include<iostream>
using namespace std;//完全展开命名空间
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}//如果在c中这里就会报错
//但是这里很明显传入的参数类型不一样,构成函数重载
//可以运行
int main()
{
cout << add(1, 2) << endl;
cout << add(1.5, 5.6) << endl;
return 0;
}
可以看到函数运行成功。
那什么样的函数构成函数重载呢?
那就是满足上面所说三点的函数是构成函数重载的。
例如下面(三数之和)
void fun(int a, char b, double c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
void fun(int a, char b)
{
cout << a << endl;
cout << b << endl;
}//函数数量不同
void fun(char a, int b, double c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}//函数顺序不同
//其余两种情况都构成函数重载
int main()
{
fun(1, 'n', 2.5);
fun(1, 'u');
fun('b', 5, 3.3);
return 0;
}
需要注意返回值不同不构成函数重载。
那么函数重载是怎么实现的呢?这就要和一个代码文件是怎么变成可执行程序有关了。
第一步预处理主要解决的就是头文件的展开,宏的替换,以及注释的消去。由test.cpp文件生成了一个test.i文件
需要注意的是这里的头文件展开和命名空间的展开是不同的,头文件的展开我的理解为将编译器原先准备好的完整头文件复制到现在所要运行的文件中,而命名空间的展开按照我的理解为在现在的空间中开辟一段空间,那段空间本身便在所要运行的文件中,并不需要复制。
第二部编译:也就是将你所写的代码翻译为汇编代码(这是一种非常接近底层的代码)这一步也是很复杂的,包含有很多的步骤:例如词法分析,语法分析,语义分析,符号汇总。生成test.s文件
第三部分也就是汇编,也就是将汇编代码转化为0和1的机器码交给cpu去运行。生成test.o文件
第四部分为链接,在写一个项目的时候不可能只包含有一个.c文件或一个.h文件,而链接的目的就是将这些不同.c文件生成的.o文件链接为.obj文件也即是可执行文件。
那么在这一个过程中就会出现当运行到一个函数的时候,call指令就要去找到这个函数的栈帧所在的位置。那么如何去找呢?这就需要符号表了,符号表里面储存的就是函数名和它地址的映射或者说是变量和它地址的映射。
那假设在写c代码的时候遇到了两个func函数,而在c语言链接的时候就是通过func这个函数名字来找地址的,而这也就导致了cpu根本不知道你要运行的是哪一个文件。如下图
而c++则不同编译器会按照会函数名修饰规则,将两个函数按照规则在符号表中用不同的名字加以区分这也就是为什么c++支持重载函数,而c不支持重载函数。
引用
在c++中引入了一个新的概念那就是引用,引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
按照通俗的话讲也就是给变量起了一个外号
例如下面的代码
#include<iostream>
using namespace std;
int main()
{
int i = 0;//这里我定义了一个整型变量i
int& a = i;//这里我给这个整型变量起了一个别名a
//对a进行++也会影响i
a++;
cout << i << endl;
printf("%p\n", &i);//然后我打印了这两个变量的地址
printf("%p\n", &a);
return 0;
}
可以看到这两个变量所代表的空间地址也是一样。
有了引用之后在很多方面是很方便的例如在之前所写的链表中,在遇到尾插如果头节点此时指向的是NULL,那么就要改变ListNode*,那么在用c传参时不得不将ListNode的地址也就是二级指针传递过来,因为若传递的是一级指针那么改变的就只是ListNode的拷贝,不能够改变ListNode*,因为在c中形参的改变并不会影响实参,但在c++中则就可以使用引用了。
struct ListNode
{
int val;
struct ListNode* next;
};
//注意这不是完整的链表尾插代码
void PushBack(struct ListNode*& head, int val)
{
struct ListNode* tmp = (struct ListNode*)malloc(sizeof(struct ListNode));//这里我简单创建一个临时空间用于尾插
tmp->val = 0;
if (head == NULL)//如果head为空那就要改变主函数中的struct ListNode*
//如果这里的head为引用那就可以直接这样写,因为引用head也就表示了head和外面的struct ListNode是同一个指针变量
{
head = tmp;
}
else//那就找尾再改变尾的next指针即可
{
//在这一步骤不需要改变指针而是要在来链表上做出改变所以使用一级指针即可
}
}
int main()
{
struct ListNode* st;//假设创建一个链表st
st = NULL;
PushBack(st, 0);
return 0;
}
可以看到st就被修改了。
除此之外引用还可以修改两数交换函数,因为引用所代表的就是原本的那个数,所以传引用也能够让两个数的值进行交换
//引用修改两数交换函数
void swap(int& p1, int& p2)
{
int p3 = p1;
p1 = p2;
p2 = p3;
}
int main()
{
int a = 50;
int b = 100;
swap(a, b);
cout<<"a = " << a << endl;
cout<<"b = " << b << endl;
return 0;
}
而能完成上面的一系列操作,因为引用就是给原变量起别名而已,这个别名仍旧代表的是原空间。
当然使用引用也是有规则的:
- 引用类型必须和引用实体是同种类型的
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
- 引用的权限可以缩小和平移但不能扩大
首先解释一下第四点
第四点也就是当一个引用已经确定一个常量/变量之后,便不能再修改这个引用,这个引用会一直代表这个常量/变量。
下面是验证代码
int main()
{
int a = 12;//这里我定义了一个变量
int& b = a;//并且我为其取了一个别名为b
int c = 30;//然后我又定义了一个c变量
b = c;//那么这句代码是会让b成为c的别名吗?
cout << "b = " << b << endl;
cout << "a = " << a << endl;
cout << "c = " << c << endl;
//显然这里并没有让b成为c的别名,而是将c复制给了a
//因为b仍然是a的别名,所以a被修改了
//如果我让c+10但是a和b仍旧不会边
c += 10;
cout << "b = " << b << endl;
cout << "a = " << a << endl;
cout << "c = " << c << endl;
return 0;
}
解释第五点
int main()
{
const int a = 10;//假设这里我定义了一个变量a并让其拥有了常属性
//那么引用时
//int& b = a;//这样就会报错
//在引用的时候也要加上const
const int& b = a;//这样才能完成引用
//而引用权限的缩小则是下面这种
int c = 0;
const int& f = c;//这样也就是权限的缩小
return 0;
}
除此之外引用类型还可以当作返回值,但是需要注意的是下面这种情况的引用返回可能会出问题。
int& func()
{
int a = 0;
a++;
return a;
}
int main()
{
int c = func();//需要注意引用最后返回的是a所在的空间但是当出func函数之后这个空间就被还给系统了
//所以这里c被赋值的到可能是1也可能是随机值,如果那片空间值没有被使用那么这里就还是1
//如果被使用了返回的就是随机值。
cout << c << endl;
return 0;
}
现在运行之后打印出的就是1
但如果是下面这样就会出现随机值
int& func()
{
int a = 0;
a++;
return a;
}
int main()
{
int& c = func();//
cout << c << endl;
cout << c << endl;
return 0;
}
可以看到出现了随机值,那么这是为什么呢?首先要知道cout也是一个函数而c也就是cout函数需要的形参。
那么此时的c也就是已经被销毁空间中的值,而第一次传递的时候还没有其它的函数使用那片空间,那么c的值任然是1,但是cout函数建立栈帧之后,就将那个空间值给覆盖了,所以第二次传递的值也就是随机数了。所以如果将func函数创建的空间很大,然后a所在的那个空间在cout函数的函数栈帧之外那么再次cout值就任然是1。下面请看证明
int& func()
{
int b[1000];//创建足够大的空间
int a = 0;
a++;
return a;
}
int main()
{
int& c = func();
cout << c << endl;
cout << c << endl;
return 0;
}
所以引用返回一般使用于在堆区new或malloc返回的空间,因为出函数之后这片空间没有还给操作系统。
写下这篇博客用于检验自己是否出现错误,所以如果您发现了任何错误,烦请告诉我,我一定修改。