C++语言规范定义
C整个语言体系简单可分成规范定义、实现与工具链以及生态系统这三个层次来理解。规范和定义负责将把需要实现的功能转换成C编程语言代码并规定文本编写以及语义等内容。实现与工具链负责把C编程代码通过编译器编译成汇编语言最终转换成计算机语言也就是二进制指令编译器有GCC、Clang和MSVCMicrosoft Visual C Compiler等编辑器有微软的Visual Studio、苹果的Xcode等编辑器。生态系统主要有实现C各种常用功能的函数的标准库。C的完整体系是一个从抽象规范到具体工具再到庞大社区的分层结构。核心语言和标准库定义了“C是什么”编译器、构建系统等工具链决定了“如何实现C”而第三方库与社区生态则赋予了C在实际项目中解决复杂问题的强大生命力。规范定义层C语言的核心蓝图核心语言Core Language语言本身的语法、语义和规则。它提供了最基础的“构建块”例如变量、数据类型、函数、控制流、类、模板、继承、多态等。实现与工具链C语言的实现工具和配置环境这一层是将你写的C代码转化为可执行程序所必需的实际工具。一个流行的C实现通常包含以下部分编辑器(Test Editor)有微软公司的集成编辑软件IDE Visual Studio、苹果公司的XCode、Linux操作系统编辑等。编译器(Compiler)负责将源代码翻译成机器码。主流的实现有GCC、Clang基于LLVM和Visual C (MSVC)。调试器(Debuggers)如GDB、LLDB用于排查程序错误。生态系统层更好的实现C语言的各种常用功能函数和数据结构的更好实现提供了C标准库C标准库Standard Library这是一个建立在核心语言之上的庞大类和函数集合为文件操作、字符串处理、数据结构等提供了现成的标准化工具。它又可以被进一步划分为标准模板库STL这是标准库中最具代表性的部分提供了容器如vector、map、迭代器和算法如sort、find等泛型编程组件。其他标准库组件包括输入/输出iostream、字符串std::string、数值计算、并发支持std::thread等。C语言规范定义C发展历史C的起源可以追溯到1979年当时Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)在贝尔实验室从事计算机工程于软件的研究工作。面对项目中复杂的软件开发任务特别是模拟和操作系统的开发任务他感受到了先有语言(如C语言)在表达能力、可维护性和可扩展性方面的不足。1983年Bjarne Stroustrup在C语言的基础上添加了面向对象的特性设计出了C语言的雏形此时的C已有了类、封装、继承等的核心概念为后来的面向对象编程奠定了基础。1 命名空间namespace1.1 namespace的价值在C/C中会有大量的变量和函数这些变量和函数有大量都是在全局作用域中这就到命名标识符存在重名的冲突为了避免命名冲突和名字污染namespace的关键字就是针对该情况。#include stdio.h #include stdlib.h int rand 20; int main() { // error C2365: “rand”: 重定义以前的定义是“函数” printf(%d, rand); return 0; }1.2 namespace的定义定义命名空间需要使用 namespace 关键字后面跟命名空间的名字然后接一对{}在{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。namespace本质是定义一个域这个域跟全局域各自独立不同的域可以定义同名变量。C中域有函数局部域、全局域、命名空间域、类域域影响的是编译时语法查找一个变量/函数/类型出处声明或定义的逻辑所以有了域隔离名字冲突也就解决了。namespace只能定义在全局当然也可以嵌套定义。项目工程中多文件中定义的同名namespace会认为是同一个namespace不会冲突。注意头文件中的声明和源文件中的定义都必须放在相同命名空间域中这是确保程序正确链接和使用的规则。C标准库都放在一个叫stdstandard的命名空间中。#include iostream #include stdlib.h #include stdbool.h // 定义命名空间 namespace WangDui_name { // 命名变量 int rand; int sdt 10; int ooo 0; // 命名函数 bool empty(const char* ch) { return ch.size 0; } int Add(int x, int y); // 命名类 class s1 { public: s1() { // ........... } private: char* arr; size_t size; size_t capacity; }; // 嵌套命名 namespace c1 { int rand 0; int b 10; } namespace c2 { int rand 90; int t 9; } }1.3 命名空间使用编译器查找变量的声明和定义时默认只会在局部域和全局域中查找不会到命名空间与中查找。所以需要使用命名空间中的变量有三种使用方式指定命名空间访问项目中推荐使用这种方式。using将命名空间中某个成员展开项目中不存在冲突的成员推荐使用。展开命名空间中的全部成员项目不推荐冲突风险很大。int main() { // 第一种指定命名空间访问方式 WangDui_name::rand 100; WangDui_name::c1::rand 300; // 第二种某个成员展开 using WangDui_name::sdt; sdt 10; // 第三种展开命名空间域全部成员 using namespace WangDui_name; c1::b 20; ooo 10; return 0; }2 C输入输出iostream是 Input Output Stream 的缩写是标准输入、输出流库定义了标准输入、输出对象。std::cin是istream类的对象它主要面向窄字符的标准输入流。std::cout是ostream类的对象它主要面向窄字符的标准输出流。std::endl是一个函数流插入输出时相当于插入一个换行符加刷新缓冲区。“”符号是流插入运算符“”是流提取运算符。#include iostream int main() { int std 90; std::cout std std::endl; int i 0; std::cin i; return 0; }#includeiostream using namespace std; int main(){ // 在io需求⽐较⾼的地⽅如部分⼤量输⼊的竞赛题中加上以下3⾏代码 // 可以提⾼CIO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); return 0; }3 缺省参数缺省参数是声明或定义函数时为函数参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该函数形参的缺省值否则使用指定的实参缺省参数分为全缺省和半缺省参数。有些地方把缺省参数也叫默认参数全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值。C规定半缺省参数必须从右往左依次连续缺省不能间隔跳跃给缺省值。带缺省参数函数调用时C规定必须从左往右依次给实参不能跳跃给实参。函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现规定必须函数声明给缺省值。#include iostream int func(int i 0, int y 0) { return i y; } int main() { func(); func(10); func(10, 20); return 0; }// stack.h 文件 #pragma once #include iostream namespace Spp { templatetypename T class stack { public: int func2(int x 10, int y 10); int func3(int i, int o 0); private: //T* array; size_t size; size_t capacity; }; class stack2 { public: int func4(int x 10, int y 10); int func5(int i, int o 0); private: int* array; size_t size; size_t capacity; }; } namespace s2 { int Add(int x, int y); int Sub(int u, int t 9); }// stack.cpp 文件 #include Stack.h namespace Spp { templatetypename T int stackT::func2(int x, int y) { return 2222; } templatetypename T int stackT::func3(int i, int o) { return 3333; } int stack2::func4(int x, int y) { return 4444; } int stack2::func5(int i, int o) { return 5555; } } namespace s2 { int Add(int x, int y) { return x y; } } int s2::Sub(int u, int t) { return 555; }// test.cpp 文件 #include Stack.h int func(int i 0, int y 0) { return i y; } using std::cout; using std::cin; using std::endl; int main() { func(); func(10); func(10, 20); cout func() endl; Spp::stack2 s2; cout s2.func4() endl; cout s2::Sub(12) endl; return 0; }4 函数重载C支持同一作用域中可以出现同名函数但是要求函数的参数不同可以是参数的个数不同或者类型不同。这样C函数调用就表现出了多态行为使用更灵活。#include iostream using std::cout; using std::cin; int Func(int x, int y) { return x y; } double Func(double x, double y) { return x y; } int Func(short x 2, int y 2) { return x y; } int main() { cout Func(1, 3); return 0; }返回值不能作为重载条件因为调用时也无法区分。#include iostream // 下⾯两个函数构成重载 // f()但是调⽤时会报错存在歧义编译器不知道调⽤谁 void f1() { std::cout f() std::endl; } void f1(int a 10) { std::cout f(int a) std::endl; } int main() { f1(); f1(10); return 0; }5 引用5.1 引用的概念和定义引用是一个已经存在的变量取一个别名编译器不会为引用变量开辟内存空间它和它的引用变量共用同一快内存空间。类型 引用别名 引用对象#include iostream using namespace std; int main() { int a 10; // 引用别名 int b a; int c a; int d a; cout a endl; cout b endl; cout c endl; cout d endl; return 0; }5.2 引用的特性引用在定义时必须初始化一个变量可以有多个引用引用一旦引用一个实体再不能引用其他实体#includeiostream using namespace std; int main() { int a 10; // 编译报错“ra”: 必须初始化引⽤ //int ra; int b a; int c 20; // 这⾥并⾮让b引⽤c因为C引⽤不能改变指向 // 这⾥是⼀个赋值 b c; cout a endl; cout b endl; cout c endl; return 0; }5.3 引用的使用引用在实践中主要是用于引用传参和引用返回值中减少拷贝提高效率和改变引用对象同时改变被引用对象。引用传参跟指针传参功能是类似的引用传参相对更方便一些。引用和指针在实践中相辅相成功能有重叠性但是各有特点互相不可替代。C的引用跟其他语言的引用(如Java)是有很大的区别的除了用法最大的点C引用定义后不能改变指向Java的引用可以改变指向。⼀些主要用C代码实现版本数据结构教材中使用C引用替代指针传参目的是简化程序避开复杂的指针但是很多同学没学过引用导致⼀头雾水。#includeiostream #include assert.h using namespace std; typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST rs, int n 4) { rs.a (STDataType*)malloc(n * sizeof(STDataType)); rs.top 0; rs.capacity n; } // 栈顶 void STPush(ST rs, STDataType x) { assert(ps); // 满了 扩容 if (rs.top rs.capacity) { printf(扩容\n); int newcapacity rs.capacity 0 ? 4 : rs.capacity * 2; STDataType * tmp (STDataType*)realloc(rs.a, newcapacity * sizeof(STDataType)); if (tmp NULL) { perror(realloc fail); return; } rs.a tmp; rs.capacity newcapacity; rs.a[rs.top] x; rs.top; } // int STTop(ST rs) int STTop(ST rs) { assert(rs.top 0); return rs.a[rs.top]; } int main() { // 调⽤全局的 ST st1; STInit(st1); STPush(st1, 1); STPush(st1, 2); cout STTop(st1) endl; }5.4 const 引用可以引用一个const对象但是必须使用const引用。const引用也可以引用普通对象因为对象的访问权限在引用过程中是可以缩小但是不能放大。需要注意的是像这些int rb a * 3; double d 12.34; int rd d; 在这种场景下a * 3 的结果会保存在一个临时对象中int rd d 也是类似在类型转换过程中会产生临时对象来存储中间值也就是说rb 和 rd 引用的是临时对象而C规定临时对象具有常性所以这里触发了权限放大必须要使用常饮用才可以。所谓临时对象是编译器需要一个空间暂存表达式的求值结果而创建的一个未命名对象C中把这个未命名对象就叫做临时对象。#include iostream int main() { const int a 10; // 报错: error C2440: “初始化”: 无法从“const int”转换为“int ” int a_a a; int b 8; // error C2440: “初始化”: 无法从“int”转换为“int 非常量引用只能绑定到左值 // 临时存储 b 3 值的未命名临时变量具有常量属性所以需要常饮用才可以初始化 int rb b 3; double d 12.34; // error C2440: “初始化”: 无法从“double”转换为“int ” // 此处存在隐式类型转换转换完成的值需要存储到临时对象中所以需要常饮用才可以初始化 int rd d; return 0; }5.5 指针和引用的关系C中指针和引用就像是两个性格迥异的亲兄弟指针是哥哥引用是弟弟在实践中他们是相辅相成功能有重叠性但各有各自的特点互相不可替代。语法上引用是给对象取别名不开辟空间指针是存储一个变量的地址要开辟空间。引用在定义时必须初始化指针建议初始化但语法上不是强制的。引用在初始化完一个对象之后就不能在改变引用对象而指针可以不断地改变指向对象。引用可以直接访问指向对象而指针需要解引用才能访问指向对象。sizeof中含义不同引用结果是指向对象类型的大小而指针结果始终是地址空间所占字节个数32位平台下占4个字节64位平台下占8个字节byte指针解引用很容易出现空指针或者野引用引用很少出现相对安全一些。6 inline用inline修饰的函数叫做内联函数编译器在调用的地方会展开内联函数这样调用内联函数就不需要建立函数栈帧提高代码的效率。inline对于编译器而言只是建议并不是强制要求能否展开内联函数由不同编译器根据不同情况判断是否展开。inline适用于频繁调用短小函数对于递归函数代码相对多些的函数加上inline编译器也会忽略。递归深度不确定编译器无法完全展开即便加了inline也基本不会展开C语言实现宏函数也会在预处理时替换展开但是宏函数实现很复杂容易出现错误且不方便调试C设计内联函数目的就是替代C的宏函数。inline不建议把函数的声明和定义分离到两个文件分离会导致链接出错。因为inline被展开就没有函数地址链接时会出现报错。在同一个项目的不同源文件中定义函数名相同但实现不同的inline函数是违反C的“单一定义规则”ODROne Defintion Rule的属于未定义行为Undefined BehaviorUB。也就是说编译器可能不报错链接器也可能不报错但程序运行结果不可预测。ODR对inline函数的硬性要求C标准规定一个inline函数可以在多个翻译单元.cpp中被定义但所有定义必须完全相同逐token相同包含函数体、参数名、返回类型、甚至空白符和注释的差异在某些情况下也可能会有影响。换句话说inline允许你“重复定义”但前提是这些定义一模一样。7 nullptrNULL实际是一个宏在传统的C头文件stddef.h中可以看到如下的代码#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endifC中NULL可能被定义为字面常量0或者C中被定义为无符号类型指针void*的常量。不论采用何种定义在使用空值的指针时都不可避免的会遇到一些麻烦本想通过fNULL调用指针版本的fint*函数但是由于NULL被定义成0调用了fint x因此与程序的初衷相悖。C11中引入nullptrnullptr是一个关键字nullptr是一种特殊类型的字面量它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题因为nullptr只能只被隐式类型转换位指针类型而不能被转换为整数类型。#include iostream using namespace std; void f(int x){ cout f(int x) endl; } void f(int* ptr){ cout f(int* ptr) endl; } int main(){ f(0); f(NULL); f((int*)NULL); f(nullptr); return 0; }