朝阳网站建设 国展,wordpress长图拼接,做网站效果图总结,网站做百度竞价1.C的发展史
C语言诞生后#xff0c;很快普及使用#xff0c;但是随着编程规模增大且越来越复杂#xff0c;并且需要高度的抽象和建模时#xff0c;C语言的诸多短板便表现了出来#xff0c;为了解决软件危机#xff0c;上世纪八十年代#xff0c;计算机界提出了oop的发展史
C语言诞生后很快普及使用但是随着编程规模增大且越来越复杂并且需要高度的抽象和建模时C语言的诸多短板便表现了出来为了解决软件危机上世纪八十年代计算机界提出了oopobject oriented programming面向对象编程思想支持面向对象的程序设计语言应运而生。
1982年Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念发明了一种新的程序语言。为了表达该语言与C语言的渊源关系命名为C。因此C是基于C语言而产生的它既可以进行C语言的过程化程序设计又可以进行以抽象数据类型为特点的基于对象的程序设计还可以进行面向对象的程序设计。
1979年贝尔实验室的本贾尼等人试图分析unix内核的时候试图将内核模块化于是在C语言的基础上进行扩展增加了类的机制完成了一个可以运行的预处理程序称之为C with classes。
语言的发展就像是练功打怪升级一样也是逐步递进由浅入深的过程。以下是C的历史版本
C with classes 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 C1.0 添加虚函数概念函数和运算符重载引用、常量等
C2.0 更加完善支持面向对象新增保护成员、多重继承、对象的初始化、抽象类、静态成员以 及const成员函数
C3.0 进一步完善引入模板解决多重继承产生的二义性问题和相应构造和析构的处理
C98 C标准第一个版本绝大多数编译器都支持得到了国际标准化组织(ISO)和美国标准化 协会认可以模板方式重写C标准库引入了STL(标准模板库)
C03 C标准第二个版本语言特性无大改变主要修订错误、减少多异性
C05 C标准委员会发布了一份计数报告(Technical ReportTR1)正式更名C0x即计 划在本世纪第一个10年的某个时间发布
C11 增加了许多特性使得C更像一种新语言比如正则表达式、基于范围for循环、auto 关键字、新容器、列表初始化、标准线程库等
C14 对C11的扩展主要是修复C11中漏洞以及改进比如泛型的lambda表达式 auto的返回值类型推导二进制字面常量等
C17 在C11上做了一些小幅改进增加了19个新特性比如static_assert()的文本信息可 选Fold表达式用于可变的模板if和switch语句中的初始化器等
C20 制定ing
C23
C26
2.C的重要作用
在开发语言排行榜上,C几乎稳居前三可见其泛用性。 下图为23年6月的 下图是2024年8月的
在工作领域C在以下领域有其独到的优势
操作系统以及大型系统软件开发服务器端开发人工智能网络工具游戏开发嵌入式领域数字图像处理分布式应用移动设备 在校招领域 不多说直接上图 笔试题网易笔试、迅雷笔试等等 面试题
从校招中公司岗位的技能要求以及学长面经总结了解到公司在校招期间更看重学生的基础最主要是语言(至少掌握一门面向对象语言java/C)、数据结构、操作系统、网络、数据库、设计模式等而本门C的授课内容更注重学生的实践动手能力、工作中的应用以及笔试面试中的技巧最后达到能够正常工作以及学习即可。
3.C的基本语法
闲话少说接下来直接进入C学习。 下面是C的关键字 asm do if return try continue auto double inline short typedef for bool dynamic_cast int signed typeid public break else long sizeof typename throw case enum mutable static union wchar_t catch explicit namespace static_cast unsigned default char export new struct using friend class extern operator switch virtual register const false private template void true const_cast float protected this volatile while delete goto reinterpret_cast 相比C语言足足多了一倍C语言32个C63个 但是这里先不细讲这些关键字在以后的学习中都会学到的。
3.1 命名空间
什么是命名空间为什么要有命名空间
在C/C中变量、函数和后面要学到的类都是大量存在的如果这些变量、函数和类的名称都存在于全局作用域中可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的。 例如在C语言中
#includestdio.h
#includestdlib.h
int rand10;
int main()
{printf(%d,rand);return 0;
}
上面这个函数会报错 它会告诉你rand是一个函数名字这里的全局变量rand有命名冲突因为在库中有了rand这个函数,再次使用rand这个名字定义变量或者函数时,编译器会分不清你到底想使用哪个rand。 可能你觉得问题不大大不了我换个名字就好了嘛但是在大型工程项目中数以MB的大小里面的变量名字可不是简简单单换个名字这么简单 C为了解决这个问题引出了命名空间这个玩法C兼容C语言的所有语法
命名空间如何定义呢
定义命名空间需要使用到namespace关键字后面跟命名空间的名字然后接一对{}即可{}中即为命名空间的成员
namespace N1 // N1为命名空间的名称
{// 命名空间中的内容既可以定义变量也可以定义函数也可以定义结构体int a;int rand;int Add(int left, int right){return left right;}struct student{char name[];int age;}
}
//2. 命名空间可以嵌套
namespace N2
{int a;int b;int Add(int left, int right){return left right;}namespace N3{int c;int d;int Sub(int left, int right){return left - right;}}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1
{int Mul(int left, int right){return left * right;}
}如最上面的N1,在这个代码中rand就定义在命名空间N1中这和库函数中定义的全局函数rand不在同一个空间中所以可以同时存在
命名空间定义后如何使用呢
namespace N
{int a 10;int b 20;int Add(int left, int right){return left right;}int Sub(int left, int right){return left - right;}
}
int main()
{printf(%d\n, a); // 该语句编译出错无法识别areturn 0;
}编译这个程序会发现编译出错编译器无法识别a那是因为我们定义了命名空间N之后并没有使用它命名空间的使用方法有以下三种 1.加命名空间名称及作用域限定符
int main()
{printf(%d\n, N::a);//::就是作用域限定符return 0;
}2.使用using将命名空间中的成员引入写项目的时候使用
using N::b;//将N::b引入全局中
int main()
{printf(%d\n, N::a);printf(%d\n, b);return 0;
}3.使用using namespace命名空间名称引入建日常使用
using namespce N;//这里相当于把N展开了也就是把N的成员加入到全局中
int main()
{printf(%d\n, N::a);printf(%d\n, b);Add(10, 20);return 0;
}当然在命名空间N1中的rand被展开之后相当于加入到全局域再使用的时候还是会和库里的rand()函数冲突 命名空间的存在奠基了C能作为创建一个大工程的语言一个大工程往往会分组安排任务C使得每个小组可以使用不同的命名空间即使命名空间定义相同的名字编译器也会帮你进行合并这对于开发者来说十分方便!
以下是一些使用 C 开发的知名游戏
《英雄联盟》这是一款非常受欢迎的多人在线战斗竞技游戏。《使命召唤》系列著名的第一人称射击游戏。《古墓丽影》系列动作冒险游戏。《星际争霸》系列经典的即时战略游戏。 以及 绝地求生****巫师三等
3.2 C的输入输出
讲到现在我们甚至还不会用C写一个“hello world”这怎么行上代码
#includeiostream
using namespace std;
int main()
{coutHello world!!!endl;return 0;
}代码中cout就是标准输出控制台除此之外还有cin——标准输入即键盘使用cout标准输出和cin标准输入时必须包含 iostream 头文件以及std标准命名空间。endl就是换行 需要注意的是早期标准库将所有功能在全局域中实现声明在.h后缀的头文件中使用时只需包含对应头文件即可后来将其实现在std命名空间下为了和C头文件区分也为了正确使用命名空间规定C头文件不带.h旧编译器(vc 6.0)中还支持iostream.h格式后续编译器已不支持因此推荐使用std的方式。 估计细心的同学已经发现了使用C进行输入输出的时候不需要增加格式控制也就是C语言格式化输入输出的%d、%c等。直接cin、cout即可。而且可以一个cincout进行连续输入输出
#include iostream
using namespace std;
int main()
{int a;double b;char c;cina;cinbc;coutaendl;coutb cendl;return 0;}
3.3 缺省参数
缺省可能单看这个名字看不出来什么意思但是找到它的英文就知道了default其实就是默认。 缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时如果没有指定实参则采用该默认值否则使用指定的实参。 缺省参数分为全缺省和半缺省 全缺省是这样的
void Func(int a 10, int b 20, int c 30){couta aendl;coutb bendl;coutc cendl;}
Func函数中a,b,c都给了默认值就叫全缺省 而半缺省长这样
void Func(int a, int b 20, int c 30){couta aendl;coutb bendl;coutc cendl;}
这里只有b和c给了缺省值而a没有就叫半缺省 需要注意的是半缺省参数只能从右往左依次给出不能间隔着给而且缺省参数不能在函数声明和定义中同时出现 //in a.h
void TestFunc(int a 10);
// in a.c
void TestFunc(int a 20)
{}
// 注意如果生命与定义位置同时出现恰巧两个位置提供的值不同那编译器就无法确定到底该用那个缺省值。而且缺省值必须是常量或者全局变量。C语言不支持这个语法
3.4 函数重载
从前有一个笑话国有两个体育项目大家根本不用看也不用担心。一个是乒乓球一个是男足。前 者是“谁也赢不了”后者是“谁也赢不了”。这个笑话表明自然语言中同样的句子可能有不同的含义而函数重载就和这个类似。 函数重载:是函数的一种特殊情况C允许在同一作用域中声明几个功能类似的同名函数这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同常用来处理实现功能类似数据类型不同的问题
int Add(int left, int right)
{return leftright;
}
double Add(double left, double right)
{return leftright;
}
long Add(long left, long right)
{return leftright;
}
int main()
{Add(10, 20);Add(10.0, 20.0);Add(10L, 20L);return 0;
}代码中有三个函数名字都为Add但是它们的参数类型不同 函数重载还可以是参数数量不同
void f()
{cout f() endl;
}
void f(int a)
{cout f(int a) endl;
}
以及参数顺序不同
void f(int a, char b)
{cout 比较航空航天大学 endl;
}
void f(char b, int a)
{cout 哈尔滨佛学院 endl;
}
但是形参名字不行
short Add(short x,short y)
{return xy;
}
short Add(short y,short x)
{return xy;
}
此外要注意返回值不同不能构成重载如
short Add(short left, short right)
{return leftright;
}
int Add(short left, short right)
{return leftright;
}那为什么C语言不支持而C支持呢
这就涉及它们整个编译的过程了在C/C中一个程序要运行起来需要经历以下几个阶段预处理、编译、汇编、链接。在这些过程中C文件和C文件的处理不同。
C语言为什么不支持 首先创建三个文件func.h func.c main.c 在.h文件中声明两个函数
int func(int x,int y);
int func(int x,double y);
这三个文件会经历 1.预处理头文件展开、宏替换、条件编译以及去掉注释这个过程结束之后func.h被展开了main.c func.c成为func.i main.i文件 2.编译语法检查和生成汇编代码func.i和main.i变成了func.s main.s(文件内容是汇编代码) 3.汇编将汇编代码转换成二进制码以便机器能够读懂此时变成func.o main.o 4.链接最关键链接时.o文件会合并在一起而且还需找一些只给了声明的函数 的函数地址而每一个.o文件都有一个符号表符号表中存放函数的地址当main文件要调用这个函数时会去符号表中找函数的地址 而符号表中两个func函数的地址编译器不知道应该调用哪个所以c程序不支持函数重载。
那C为什么支持呢 相比起C程序而言,C新增了一个函数名修饰规则来支持函数重载这个规则就是将函数的参数带入符号表所以参数的类型,数量,顺序不同代表的是不同的函数,找地址时就不会出错! 将C代码转到反汇编我们可以看到 函数参数的类型,数量,顺序不同那么对应在符号表中的名字就不一样main文件再去找函数地址时就不会冲突。这个命名规则C标准并没有具体规定由每个编译器自己决定如VS2022就与g不同VS2022的命名规则比较诡异但是一定可以保证的是参数不同的函数名字不同 对比C语言c程序符号表中只有一个函数名函数参数没有参与进来所以C程序不支持相同函数名的函数。
那么这时候就会有同学问了如果在符号表命名规则中加入返回值不就可以让不同的返回值支持函数重载了吗 答案是不能 因为在调用的时候并没有返回值比如下面这两个函数
void add(double x, double y)
{return ;
}
double add(double x, double y)
{return xy;
}
double a0.5,b0.5;在调用的时候我们只有add(a,b),那么即使符号表的问题解决了调用规则摆在这还是无法确定应该调用符号表中的哪个函数。除非你把调用规则也改掉让它在调用的时候带上返回值一起
有时候在C工程中可能需要将某些函数按照C的风格来编译在函数前加extern “C”意思是告诉编译器将该函数按照C语言规则来编译。比如tcmalloc是google用C实现的一个项目他提供tcmallc()和tcfree两个接口来使用但如果是C项目就没办法使用那么他就使用extern “C”来解决。
extern C int Add(int left, int right);
int main()
{Add(1,2);return 0;
}此时就会链接时报错error LNK2019: 无法解析的外部符号_Add该符号在函数 _main 中被引用 因为函数按照C语言的规则来命名。
下面两个函数构成函数重载吗
void TestFunc(int a 10)
{coutvoid TestFunc(int)endl;
}
void TestFunc(int a)
{coutvoid TestFunc(int)endl;
}答案是不构成因为对于TestFunc(int)编译器根本不知道应该调用哪个函数,会直接报错
3.5 引用
众所周知指针是C语言的精髓所在引用之于C犹如指针之于C甚至C不仅支持指针java、python等语言都不支持指针而且支持引用可谓卧龙凤雏齐聚也。
什么是引用 引用不是新定义一个变量而是给已存在变量取了一个别名。 比如孙悟空又叫孙行者又叫齐天大圣又叫弼马温又叫斗战圣佛。 再比如我除了我的名字又叫全世界最帅的人bushi。 但是编译器不会为引用变量开辟内存空间它和它引用的变量共用同一块内存空间。
int a10;
int ba;
printf(%p\n%p,a,b);引用是怎么使用的 定义一个引用 类型 引用变量名(对象名) 引用实体
void TestRef()
{int a 10;int ra a;//定义引用类型printf(%p\n, a);printf(%p\n, ra);
}引用类型需要与引用实体同类 引用有三个特点 1.必须在定义时初始化 2.一个变量可以有多个引用 3.一个引用一旦有了实体就不能再引用其他实体
void TestRef()
{int a 10;// int ra; // 该条语句编译时会出错int ra a;int rra a;printf(%p %p %p\n, a, ra, rra);
}对于常量的引用
void TestConstRef()
{const int a 10;//int ra a; // 该语句编译时会出错a为常量const int ra a;// int b 10; // 该语句编译时会出错b为常量const int b 10;double d 12.34;//int rd d; // 该语句编译时会出错类型不同const int rd d;
}为什么不能直接引用一个常量呢
这是因为引用的一个用处是引用改变的时候实体也会跟着改变因为它和引用实体占用同一个地址空间就比如你让孙行者带上金箍那孙悟空是不是也带上了金箍
int a 10;
int b a;
cout b a endl;
b 20;
cout b a endl;可见a的值也被修改成了20。 但是常量之所以是常量就是因为它不能被随意修改如果用
const int a10;
int raa;那么a就存在被修改的可能性对于a来讲权限就被放大了但是这样的权限放大是很危险的所以有一个原则可以进行权限缩小或者平移但是不能扩大。所以我们可以像下面这样
//平移
int a0;
int raa;
//平移
const int b10;
const int rbb;
//缩小
const int ra1a;除了作为一个引用变量/常量引用还有哪些使用场景呢
作函数参数
之前在写C程序的交换函数时因为形参是实参的一份拷贝想要改变实参就要传地址而现在有了引用就不用传地址了!
void Swap(int left, int right)//交换函数
{int temp left;left right;right temp;
}int a 10;
int b 20;
Swap(a,b);//因为形参为引用,所以这传的是a,b的引用所以形参的改变就是实参的改变
但是注意不要这样传参Swap(a,b)因为这样传进去的是地址兼容C语言广泛地说跟在类型后面是引用跟在变/常量的前面是取地址。
作返回值 引用作为返回值时可以在函数外面修改函数里面的内容前提是引用的变量出了函数也不会销毁
static int n 0;
int Count1()
{n;n;return n;
}int Count2()
{int m0;m;return m;
}
int tmp Count1();
int tem Count2();
tmp 20;
tem 10;
cout tmp n tem m; 这你Count2函数所在的空间为栈空间由函数栈帧的有关知识可知进入函数时创建栈帧m变量也在栈帧中在出了函数之后其栈帧会被销毁m的地址也会被释放所以这是tmp的空间其实被释放过我愿称之为野引用而n由于存在静态区就不会被释放。 甚至给tem赋值也不会有变化 所以如果实体在出函数后会被销毁的时候需要传值返回。
看到这你有没有感觉这根指针很像他们直接肯定有联系 引用和指针的联系 引用在语法概念上就是一个别名和实体共用一份空间 但是引用在底层实现上是有空间的因为引用是按照指针的方式来实现的也就是说指针的底层汇编和引用一样。 简直一模一样 但是它俩也有区别
引用在定义时必须初始化指针没有要求引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体没有NULL引用但有NULL指针在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小有多级指针但是没有多级引用访问实体方式不同指针需要显式解引用引用编译器自己处理引用比指针使用起来相对更安全指针比引用更灵活 总的来说C中更喜欢使用引用特别在一些容器中比如栈、队列等在类和对象中也十分常见。
3.6 空指针nullptr
在C98中NULL表示空指针一般在我们初始化一个指针的时候会用NULL但实际上NULL是一个宏在stddef.h中定义
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif可以看到NULL可能被定义为字面常量0或者被定义为无类型指针(void*)的常量但不论采取何种定义在使用空值的指针时都不可避免的会遇到一些麻烦比如下面这段代码
void f(int)
{coutf(int)endl;
}
void f(int*)
{coutf(int*)endl;
}
int main()
{f(0);f(NULL);f((int*)NULL);return 0;
}程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0因此与程序的初衷相悖。 在C98中字面常量0既可以是一个整形数字也可以是无类型的指针(void*)常量但是编译器默认情况下将其看成是一个整形常量如果要将其按照指针方式来使用必须对其进行强转(void *)0。
但是在C11中nullptr作为一个关键字被引入所以使用它时不需要包含头文件而且它就是void*0,这里建议以后进行C的编程时表示空指针都用nullptr
3.7 内联函数
什么叫内联函数 以inline修饰的函数叫做内联函数编译时C编译器会在调用内联函数的地方展开没有函数压栈的开销内联函数提升程序运行的效率。 在学习函数栈帧的时候我们了解到在汇编代码中函数的调用一定会用到call指令
int Add(int left, int right)
{return leftright;
} 就是return 0 上面两行那个call
inline int Add(int left, int right)
{return leftright;
} 此时就没有call了直接把Add函数内部的搬过来了 所以内联函数的本质就是用函数体替换函数调用。 内联函数可以减少调用提高运行效率但因为可能把函数体多次展开也可能使目标文件变大。 如果要是把一个很大的函数体多次展开那岂不是十分冗杂 编译器也想到了坐在电脑前的程序员可能是个笨蛋比如我所以它可以选择不展开。 inline对于编译器而言只是一个建议若一个函数代码很长则编译器不会将它变成内联 一般来说,函数代码在10行及以内时这时编译器会将它优化为内联函数有些编译器是在30行以内。 此外需要注意的是 内联函数的定义和声明不能分开。因为inline被展开后就没有函数地址了,链接时会找不到。 内联函数可以和宏替换有点类似在某些宏替换的地方定义一个内联函数也可以 3.8 auto关键字
在C的中后期你可能会见到这样的代码
__list_iteratorInputTterator::iterator it tmp.begin();
其中__list_iterator是模版 InputTterator类型用来实例化类 iterator类的成员 it 变量 好长太麻烦了不想写这么多 怎么办 可以这样写
__list_iteratorInputTterator::iterator it tmp.begin();
//化简后
auto it tmp.begin();
为什么可以这样写 这是因为auto是一个特殊的类型可以像int char那样使用但是这个类型是由编译器自己推导出来的比如
int a 10;
auto b a;
auto c a;
这里编译器推导出来 b是int而c 是char。
auto还有一些其他的使用规则 1.对指针和引用的区别
auto对指针来说“可有可无”
int x 10;
auto a x;
auto* b x;
观察这段代码a是int* 类型此时auto也是int*b也是int类型此时auto就是int所以如果对象是指针使用auto时加/不加都可以
但对引用来说则必须加
int x 10;
auto c x;
auto d x;
c的类型是int的引用,此时的auto是intd的类型是int,此时的auto是int。所以如果要用auto创建一个引用变量请加上符号
2.一行多次定义 当在同一行声明多个变量时这些变量必须是相同的类型否则编译器将会报错 因为编译器实际只对第一个类型进行推导然后用推导出来的类型定义其他变量
auto a 1, b 2; //没问题
auto c 3, d 4.0; // 该行代码会编译失败因为c和d的初始化表达式类型不同
事实上,当第一个变量被推导成int后第二个变量默认也是int.但是int类型不能存放double类型的值所以编译失败因此也可以推断出
auto a 1, b 2; //没问题
auto c 3, d a; // 该行代码会编译失败因为c和d的初始化表达式类型不同
是没有问题的
3.auto无法使用的地方 1函数参数
void TestAuto(auto a)
{//...
}
这个不用多说因为编译器无法确定它的实际类型 2声明数组
int a[]{1,2,3};
auto b[]{2,3,4};
直觉上似乎b数组里都是整型这完全可以推导出来啊。但是事实上不能原因 其实auto只和下面讲的范围for在一起用的时候比较常见其他时候不常见
3.9 基于范围的for循环
看下面这个代码
int arr[] {1,2,3,4,5,6};
for(auto e : arr)
{coute ;
}
咦似乎跟我们平时用的for不太一样 是的这就是C11引入的范围for循环for循环后的括号由冒号“ ”分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。范围for不仅是可读的也是可写的方法是用引用变量进行迭代
void TestFor()
{int array[] { 1, 2, 3, 4, 5 };for(auto e : array)e * 2;for(auto e : array)cout e ;return 0;
}范围for循环的要求
for循环迭代的范围必须是确定的 对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的 方法begin和end就是for循环迭代的范围。后面会讲 以下代码就有问题因为for的范围不确定
void TestFor(int array[])
{for(auto e : array)cout e endl;
}迭代的对象要实现和的操作。(如果不能和判相等还怎么迭代)