初识C++和C语言缺陷补充
一.主要内容:
1.对于C++ 来说是在C语言的基础之上刚开始进行对C语言的缺陷进行了优化解决,加入面向对象的编程思想,加入了非常多有用的库,和一些编程范式。对于C语言比较属性那么对于C++的学习是有帮助的。
2.那么C++对于C语言的一些些缺陷是在那一些方面呢?包括有作用域,IO方面,函数方面,指针,宏方面进行了修改。
二.具体内容:
一: 作用域
1.命名空间:
1.在C语言中存在这样的一个情况我们使用一个全局变量的时候有可能在命名变量名称的时候和C语言标准库中的内容产生冲突(1.我们的标准库在全局展开会和自己的变量名称冲突 2.在我们做一个项目的时候我们定义的变量名函数名和其他人会有冲突)。
2.解决方法:使用命名空间这一个C++提供的语法:namespace关键字,它可以把我们本地定义的内容进行包裹,在正常开始访问变量的时候默认只访问全局。下面这个代码没有进入命名空间在全局只有一个rand函数所以打印出来了一个随机值。
3.如何使用自己的命名空间呢?
:: 域作用限定符 命名空间名称+域作用限定符去访问这个命名空间。命名空间中可以放什么东西呢?
变量名,函数声明,函数定义,结构体,联合体,枚举,等等。
4.如果出现了在一个命名空间中有两个这个同名称的变量或者函数等等。非常容易解决这个问题因为我们C++是支持命名空间的嵌套的:
2.函数声明和定义:
1自己的函数声明放在自己的命名空间中自己的定义放在自己的命名空间中在我们进行定义和声明分离的时候我们要把定义和声明放在同一个空间中:
c
#include"Stack.h"
namespace sfpy_stack {
//函数定义:
//1.初始化:
void InitStack(struct Stack* st1)
{
assert(st1 != NULL);
StData* tmp = (StData*)malloc(sizeof(StData) * 4);
if (tmp == NULL)
{
perror("malloc file");
exit(-1);
}
st1->st = tmp;
st1->top = 0;
st1->capacity = 4;
}
//2.插入数据:
void StackPush(struct Stack* st1, StData x)
{
if (st1->top == st1->capacity)
{
StData* tmp = (StData*)realloc(st1->st,(sizeof(StData)*((st1->capacity)*2)));
if (tmp == NULL)
{
perror("malloc file");
exit(-1);
}
st1->st = tmp;
st1->capacity = (st1->capacity) * 2;
}
st1->st[st1->top] = x;
st1->top++;
}
//3.数据遍历:
void StackPrint(struct Stack* st1)
{
assert(st1 != NULL);
for (int i = 0; i < st1->top; i++)
{
printf("%d ", st1->st[i]);
}
}
//4.栈销毁
void StackDestory(struct Stack* st1)
{
free(st1->st);
st1->st = NULL;
}
}
??????????????????????????????????????????
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
namespace sfpy_stack {
typedef int StData;
typedef struct Stack{
StData* st;
int top;
int capacity;
}sk;
//函数声明:
//1.初始化:
void InitStack(struct Stack* st1);
//2.插入数据:
void StackPush(struct Stack* st1, StData x);
//3.数据遍历:
void StackPrint(struct Stack* st1);
//4.栈销毁
void StackDestory(struct Stack* st1);
}
#include"Stack.h"
int main()
{
//1.定义一个结构体:
struct sfpy_stack::Stack st;
//1-1:在有struct的情况下
//我们定义结构体变量使用上面这个方法
//sfpy_stack::struct Stack st2;
//1-2:重命名之后就可以使用下面这个方法:
//sfpy_stack::sk st2;
//2.进行初始化:
sfpy_stack::InitStack(&st);
//3.数据插入:
sfpy_stack::StackPush(&st, 1);
sfpy_stack::StackPush(&st, 2);
sfpy_stack::StackPush(&st, 3);
sfpy_stack::StackPush(&st, 4);
sfpy_stack::StackPush(&st, 5);
sfpy_stack::StackPush(&st, 6);
//4.栈数据的打印:
sfpy_stack::StackPrint(&st);
//4.进行销毁:
sfpy_stack::StackDestory(&st);
return 0;
}
3.不存在命名冲突的情况:
1.如何打开命名空间:使用using namespace spfy_stack;
2.打开命名空间就可以不使用域限定操作符直接使用变量函数等。
3.在我们的个人作业不存在命名冲突的情况使用using namespace spfy_stack ,可以在你去使用命名空间内容就可以不使用域限定操作符:
二.输入输出:
1.基本输入输出:
1.C++ 官方标准库iostream。
2.我们的std空间名称是C++的官方定义的输入输出相关的命名空间。
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
- <<是流插入运算符,>>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
- 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识。
2.关于std的展开:
1.std是C++ 标准库中的一个命名空间他的展开是需要考虑情况的因为对于一个由多个人完成的项目来说这个空间是不可以展开的。
2。在自己的练习上面去使用using namespace std;打开标准库的这一个命名空间,这样可以不需要去使用标准库名称和域限定操作符。
3-1:换行的多种方法:
3-1:1.在字符串中结尾使用\n. 2.在输出到控制台的单词后面加一个"\n".3.使用endl结尾这个是std这个命名空间中的一个用来换行的根第2个是同样的效果。
endl==end line
4:如果存在一种情况需要换行多次但是其他的std中的有冲突(不允许完全打开命名空间)我们可以只打开命名空间中的一个操作符。
三.函数:
1.缺省参数(部分):
如果我们给这个函数的这个变量进行了传参我们就使用我们的传参,如果我们没有去进行传参就使用默认的缺省参数。
2.作用:我们开辟栈空间知道要看多少空间和不知道需要开多少空间:
2-1:不知道要开多少空间我们就使用默认开辟空间的值后面动态增长:
2-2:知道要开多少空间我们就使用传参的值一下开辟好空间不需要多次判断扩容:
2-3:函数相同在存在一个缺省值的情况下完成了多个功能。
注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
2.全缺省:
1.函数的参数全部都具有缺省值这样我们对于一个函数就有n+1种调用方式:为什么可以使用相同的函数名称呢?我们接下来再说!
3.缺省参数的定义和参数的传递
1`.缺省参数值只能从右往左给。
2.函数传实参只能从左往右。
3.如果我们给了全部缺省我们不能跳跃的进行实参传递。
4.函数的声明和定义中不要同时出现缺省参数我们一般在函数的声明中给缺省值,如果函数的定义和声明中都有缺省值我们的编译器不知道使用哪一个缺省值。
4.函数重载:
1.函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表==(参数个数 或 类型 或 类型顺序)==不同,常用来处理实现功能类似数据类型不同的问题。
>1.参数个数:
>2.类型:
>3.类型顺序:
>4.总结:如果存在一种情况:
事情A单独出现必须去做。
事情B单独出现必须去做。
事情A和B同时出现怎么办?编译器面对这样的情况不知道怎么办的那就报错:
A:妈妈掉水里了 B:爸爸掉水里了
5.为什么出现函数重载?(编译链接的原理):
为什么C++支持函数重载但是C语言不支持函数重载呢?
定义:Func.cpp
声明:Func.h
主函数:text2.cpp
c
//Func.cpp
int add(double b, int c)
{
return b + c;
}
int add(int b, double c)
{
return b + c;
}
//Func.h
#include<iostream>
int add(double b, int c);
int add(int b, double c);
//text2
#include"Func.h"
using namespace std;
int main()
{
//1.参数不同:
cout << add(2.2, 3) << endl;
cout << add(2, 3.3) << endl;
return 0;
}
这里我们通过linux进行预编译,编译,汇编,链接整个过程:
1.预编译(预处理)
1.头文件的展开,宏替换,条件编译,去掉注释。
生成:通过预编译生成==func.i test2.i文件
2.编译
2.编译进行:语法分析,词法分析,语意分析,符号汇总。
生成:通过编译生成汇编代码:Func.s text2.s
通过vim观察一下汇编代码我们看一看两个函数的地址名称:
关于linux下的函数定义的汇编代码通过什么方法命名的呢?
_Z + (函数名字符个数) +(函数名称)+参数类型的首字母:
通过观察下面两个函数名称我们就可以看出来!!!
通过vs上的反汇编我们可以观察到两个函数定义call的地址是不相同的!
在vs下定义相同函数名称的函数重载的方法比较linux下的是,windows下是通过特殊符号(对应一个类型)进行的函数名称的汇编定义!!
3.汇编
在linux下汇编后生成的二进制文件是是以.o结尾的文件,在linux下生成的是以.obj
为后缀的二进制文件。
4.链接:
链接机器会把各种.o文件和并对文件需要的库进行链接生成可执行的二进制文件 .out
如果我们的函数没有定义在func.cpp中那么到链接的时候会显示链接错误说明链接错误是发生在链接的过程中!!!!。这个时候我们的代码已经生成了二进制的机器码了。但是在链接的过程中找不到函数的定义了!!!
6.引用:
1.基本概念:
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
1-1:关于函数的引用:
1-2:引用用的特殊:
1.引用必须要初始化:
2.一个变量可以被多次引用:
3.一个引用只能被初始化一次之后如果a=b(a已经引用初始化过了b对a是赋值操作)
1-3:常引用:
1.常引用:常表示的是常变量。
2.给一个常量取别名:
2-1:访问打印 或者 进行改变(常量是不可以改变的)
图1
从图1到图2是一种权限的缩小,原来的引用可以干两个事情
1.通过引用改变--引用的变量的值。2。获取引用变量的值进行打印:
3.图二中的引用只能做(获取引用变量的值进行打印)
图2:
图三:
图四:
图五:
图6
图7:
图8:
总结我们观察上面的两个图片我们会发现引用和指针在汇编实现的时候是相同的但是题目有不同的作用:
1.在语法上r是ch的别名改变r就是改变ch
2.但是在底层r是开辟了一个4字节空间用来保存地址:
2.常规用法:
1.作为函数参数:
swap函数用来交换数值:
2.作为函数返回值:
如何保存函数返回值:
1.函数返回值比较小的时候就用一个寄存器去保存函数返回值:
2.函数返回值比较大的时候在main和函数栈帧空间中开辟一个位置保存这个返回值。
3ret的值取绝于函数栈帧销毁之后是否清理空,清理的化就是随机值不清理就是上面的情况但是范围一个不属于当前代码的空间是比较危险。
怎么样引用才可以作为函数返回值呢:
1.这个变量在函数栈帧销毁的时候它不会被销毁:
1-1:static静态变量(存放在静态区)
1-2:开辟在堆区的变量(存放在堆区)
3.特殊用法:
1.我们在模拟实现单链表的时候因为进行开始的头节点的改变和头插都需要改变头节点而传了一个二级指针的操作:
1.解决方法一:二级指针:
我们通过二级指针的方法就可以直接改变我们头节点的地址:
2.解决方法二:引用:
通过上面的知识我们知道int是可以有别名的int*有别人名的。当然结构体类型有别名的。结构体指针类型有别名。
4.总结:
1.引用在语法上就是另一个变量的别名,指针是存储了另一个变量的地址。
2.引用必须初始化,指针无所谓初始化。
3.引用初始化是一种传址,后面的赋值都是一种改变变量数值的操作。
4.引用初始化一个变量之后就不可以在引用其他变量,然鹅指针可以在任何时候指向另一个同类型的实体:
5.sizeof 中的含义不同因为我引用是你的别名应该有你的所有属性所以sizeof大小应该满足引用类型的字节大小,指针类型是地址地址在32为平台下就是4字节:
6.引用++ 就是引用的实际个体数值++,指针++就是向后移动应该类型的字节大小。
7.有多级指针但是没有多级引用。
8.访问实体的方式不同,指针靠解引用,引用靠编译器自己处理。
9.引用比指针使用起来更加安全,这个安全是相对的内容。
7.内联函数:
关于宏:
优点:
1.不需要开辟函数栈帧,节省空间。
2.宏的效率会快一些。
缺点:
1.容易出现优先级的问题。
2.不可以调试
3.宏的内容过多会产生空间的浪费。
4.宏是没有类型检查的。
1.概念:
一个inline加在一个函数的前面就让这个函数获得了宏的所有优点但是并没有宏的一些缺陷(优先级,函数调试,类型检查),内联提高了函数的运行效率:
1.因为inline是一个对代码的优化,默认不会打开这个代码优化。
打开之后的结果:
2.inline的优缺点:
1.inline是具有不错的优点但是inline不是一个非常好的处理方法。
2.inline本质上是一个用空间换时间的做法,如果编译器把函数当作内联函数处理。那么在代码进行预编译的时候我们就进行替换,好处:节省了调用函数栈帧的时间消耗,缺点:导致目标文件变大。
3.inline对于编译器只是一个建议,不同的的编译器对于inline的实现的机制可能不同,一般函数比较小,不是递归并且频繁调用的函数可以去用inline修饰。
4.inline不建议定义和声明分开会导致连接错误因为inline函数需要展开但是如果定义和声明分离展开是没有内容的(找不到函数地址链接找不到的)。
5.优点:
1.有宏的所有优点,没有宏的缺点
2.频繁调用一个小函数效率是比较高的:
6.缺点:
1.内联函数比较大,就会大大使用了目标空间。
2.使用一个较大函数我们内联就都会去编译函数代码,对于正常的函数调用只会编译一条函数代码。