个人专栏《数据结构-初阶》《经典OJ题目》《C语言》《小白算法成长录》欢迎大佬交流本文代码已同步GitHubGitHub - Stellen-z/DailyCode: pracetice · GitHub一、从C语言到C1、为什么学习CC是在C语言基础上发展出来的语言它不仅保留了C的高性能直接操作内存的能力适合底层开发的特性还增加了面向对象编程OOP泛型编程模板标准模板库STL简单理解C是“工具”C是“工具箱 设计系统”学习C的意义是从“过程式思维” → “结构化 对象化思维”从“写单个函数” → “设计完整程序结构”为后续高阶数据结构与算法打基础2、C和C的区别C语言面向过程编程C语言以“函数”为核心把问题拆解为一个个步骤程序 函数 数据强调“怎么做”过程C多范式语言面向对象为主C支持面向过程兼容C面向对象OOP泛型编程模板更强调“谁来做这件事”对象C语言不支持没有类class没有对象没有封装 / 继承 / 多态C支持class / struct增强版封装封装数据和方法继承代码复用多态接口统一3、第一个C程序首先明确C兼容C绝大多数的语法因此我们先在C环境中编译C代码#include stdio.h int main() { printf(Hello World!\n); return 0; }程序正常运行当然C有自己的输入输出肯定不会像C语言这样写#include iostream using namespace std; int main() { cout Hello World! endl; return 0; }不知道各位小伙伴用C/Java第一次写出Hello World时和第一次用C语言写出来的Hello World有什么不同的感觉呢下面我们就从C的第一个程序开始学习基础语法二、C基础语法1、namespace我们先来看一段C语言程序#include stdio.h #include stdlib.h int rand 10; int main() { //error C2365: “rand”: 重定义以前的定义是“函数” printf(%d\n, rand); return 0; }直接编译错误报错原因是重定义当我们想打印 rand 时在C语言的编译环境下、rand是一个函数而我们在全局变量中又定义了rand导致rand的重定义那么在C中我们该怎么避免这种情况呢答案就是使用 namespace--命名空间namespace 的定义● 定义命名空间需要使用namespace 关键字后面跟命名空间的名字然后加上一对{ }{ }中即为命名空间的成员可以定义变量、函数、类等● namespace 本质是定义一个域这个域跟全局域各自独立而不同的域可以定义同名变量因此基本解决了命名冲突● C中域有函数局部域全局域命名空间域类域域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑因此有了域隔离名字冲突就解决了局部域和全局域除了会影响编译查找逻辑还会影响变量的生命周期命名空间域和类域不影响变量生命周期。● namespace 只能定义在全局当然也可以嵌套定义● 在项目工程多文件中定义的同名 namespace 会认为是一个 namespace 不会冲突● C标准库都放在一个叫 std(standard) 的命名空间中下面我们来定义上述的例子//1.命名空间定义 #include stdio.h #include stdlib.h namespace stn { //定义变量 int rand 20; //定义函数 int Add(int x, int y) { return x y; } } int main() { printf(%p\n, rand);//默认访问rand函数指针 printf(%d\n, stn::rand);//访问stn命名空间中的rand return 0; }//2.命名空间的嵌套 #include stdio.h #include stdlib.h namespace mn { //pq命名空间 namespace pq { int rand 1; int Add(int x, int y) { return x y; } } //xy命名空间 namespace xy { int rand 1; int Add(int x, int y) { return x y; } } } int main() { printf(%d\n, mn::pq::rand); printf(%d\n, mn::xy::rand); printf(%d\n, mn::pq::Add(1, 2)); printf(%d\n, mn::xy::Add(1, 2)); return 0; }//多文件可以重复定义同名的namespace他们会默认合并到一起 //Stack.h #pragma once #include stdio.h #include stdlib.h #include assert.h #include stdbool.h namespace stn { // 支持动态增长的栈 typedef int STDataType; typedef struct Stack { STDataType* _a; int _top; // 栈顶 int _capacity; // 容量 }Stack; // 初始化栈 void StackInit(Stack* ps); // 入栈 void StackPush(Stack* ps, STDataType data); // 出栈 void StackPop(Stack* ps); // 获取栈顶元素 STDataType StackTop(Stack* ps); // 获取栈中有效元素个数 int StackSize(Stack* ps); // 检测栈是否为空如果为空返回非零结果如果不为空返回0 bool StackEmpty(Stack* ps); // 销毁栈 void StackDestroy(Stack* ps); }//Stack.cpp #include Stack.h namespace stn { // 初始化栈 void StackInit(Stack* ps) { assert(ps); ps-_a NULL; ps-_capacity ps-_top 0; } // 入栈 void StackPush(Stack* ps, STDataType data) { assert(ps); if (ps-_capacity ps-_top) { int newcapacity ps-_capacity 0 ? 4 : 2 * ps-_capacity; STDataType* tmp (STDataType*)realloc(ps-_a, sizeof(STDataType) * newcapacity); if (tmp NULL) { perror(realloc failed!\n); exit(1); } ps-_a tmp; ps-_capacity newcapacity; } ps-_a[ps-_top] data; } // 出栈 void StackPop(Stack* ps) { assert(ps); assert(ps-_top 0); ps-_top--; } // 获取栈顶元素 STDataType StackTop(Stack* ps) { assert(ps); assert(ps-_top 0); return ps-_a[ps-_top - 1]; } // 获取栈中有效元素个数 int StackSize(Stack* ps) { assert(ps); return ps-_top; } // 检测栈是否为空如果为空返回非零结果如果不为空返回0 bool StackEmpty(Stack* ps) { assert(ps); return ps-_top 0; } // 销毁栈 void StackDestroy(Stack* ps) { free(ps-_a); ps-_a NULL; ps-_capacity ps-_top 0; } }//Test.cpp #include Stack.h //定义全局的Stack typedef struct Stack { int a[10]; int top; }ST; void STInit(ST* ps) {}; void STPush(ST* ps, int x) {}; int main() { //调用全局的栈 ST st1; STInit(st1); STPush(st1, 1); STPush(st1, 2); STPush(st1, 3); printf(%d\n, sizeof(st1));//固定10个int类型共44字节 //调用stn的栈 stn::Stack st2; printf(%d\n, sizeof(st2));//一个x64环境下指针,两个int类型共16字节 stn::StackInit(st2); stn::StackPush(st2, 1); stn::StackPush(st2, 2); stn::StackPush(st2, 3); }namespace 的使用当编译器在查找一个变量的声明/定义时默认只会在局部或者全局查找不会到命名空间中查找因此下面的程序会报错#include stdio.h namespace stn { int a 0; int b 1; } int main() { //error C2065: “a”: 未声明的标识符 printf(%d\n, a); return 0; }要使用命名空间中定义和变量、函数有三种方式a、指定命名空间项目中推荐这种方式int main() { printf(%d\n, stn::a); return 0; }b、using将命名空间中某个成员展开项目中经常访问且不存在冲突的成员推荐这种方式using stn::a; int main() { printf(%d\n, a); printf(%d\n, stn::b); return 0; }c、展开命名空间全部成员项目不推荐冲突风险很大日常练习可以使用using namespace stn; int main() { printf(%d\n, a); printf(%d\n, b); return 0; }2、输入输出在C语言中包含输入输出的头文件是stdio.h而在C中包含输入输出的头文件是iostreamiostream是 Input Output Stream的缩写是标准的输入输出流库定义了标准的输入输出对象而在前面我们说过std这个命名空间里面包含cin 和 coutstd::cin 是 istream类的对象主要面向的是窄字符(narrow characters)的标准输入流那么宽字符对应的就是wcin;std::cout 是 ostream 类的对象主要面向窄字符的标准输出那么宽字符对应的就是wcout是流插入运算符是流提取运算符std::endl 是一个函数流插入输出时相当于一个换行符加刷新缓冲区使用C输入输出更方便不需要printf/scanf那样需要手动指定格式C的输入输出可以自动识别变量类型cout/cin/endl等都属于C标准库C标准库都放在std的命名空间中因此要通过命名空间来使用因此在日常练习中我们可以 using namespace std实际项目开发中不能直接展开由于C兼容C大部分语法因此在编译时会一并编译C语言的代码这就导致有时C的效率会降低比如当需要处理大量输入输出时为了提高效率我们通常会加上下面这三行代码#include iostream int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); std::cout.tie(nullptr); return 0; }三行代码的作用std::ios_base::sync_with_stdio(false);取消 C 标准流cin/cout与 C 标准流stdio之间的同步默认情况下两者同步以保证混用时的顺序正确但会带来额外开销。关闭同步后cin/cout效率大幅提升但不能再混用printf/scanf否则会导致未定义行为。std::cin.tie(nullptr);解除cin与cout的绑定。默认情况下每次执行cin前会自动刷新cout解除绑定可减少不必要的刷新开销。std::cout.tie(nullptr);解除cout与其他流的绑定通常无额外绑定但保持写法对称或防止未来修改。三、函数增强1、缺省参数缺省参数的定义允许在函数声明时为参数指定默认值调用时若省略该实参则自动使用默认值。缺省分为全缺省、半缺省全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值注C规定半缺省参数必须从右向左依次连续缺省不能间隔跳跃给缺省值函数定义和声明分离时缺省参数不能在函数声明和定义中同时出现规定必须函数声明给缺省值我们来看下面的例子就能明白a、缺省参数的使用#include iostream using namespace std; void fun(int a 1) { cout a endl; } int main() { fun(); //没有传参时使用参数的默认值 fun(10); //传参时使用指定的实参 }b、缺省参数的分类#include iostream using namespace std; //全缺省 void Func1(int a 1,int b 2,int c 3) { cout a b c endl; } //半缺省 void Func2(int a, int b 2, int c 3) { cout a b c endl; } int main() { Func1(); //1 2 3 Func1(10); //10 2 3 Func1(10,20); //10 20 3 Func1(10,20,30); //10 20 30 Func2(100); //100 2 3 Func2(100,200); //100 200 3 Func2(100,200,300); //100 200 300 }2、函数重载C支持在同一作用域中出现同名函数但是要求这些同名函数的形参不同可以是参数个数不同或者是类型不同注C语言是不支持同一作用域出现同名函数的我们通过代码来观察#include iostream using namespace std; namespace stn { //1.参数类型不同 int Add(int x, int y) { cout Add(int x, int y) endl; return x y; } double Add(double x, double y) { cout Add(double x, double y) endl; return x y; } //2.参数个数不同 void func(int a) { cout func(int a) endl; } void func(int a, int b) { cout func(int a,int b) 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() { stn::Add(1, 2); stn::Add(1.1, 2.2); stn::func(1); stn::func(1,2); stn::f(1, x); stn::f(x, 1); return 0; }既然参数类型、参数个数、参数顺序都会构成函数重载那么函数返回值是否会构成重载呢我们来试一下#include iostream using namespace std; int f() { cout f() endl; return 1; } //error C2556: “void f(void)”: 重载函数与“int f(void)”只是在返回类型上不同 void f() { cout f() endl; } int main() { f(); f(); return 0; }编译失败编译器在调用时不知道到底调用哪一个函数最后我们来看下面这组代码#include iostream using namespace std; void f() { cout f() endl; } void f(int a 10) { cout f(int a 10) endl; } int main() { f();//error C2668: “f”: 对重载函数的调用不明确 f(10); return 0; }首先来看这两个函数是否构成函数重载答案是构成那为什么会报错呢当全缺省函数和无参函数都存在时如果调用函数时没有进行传参就会引发歧义编译器不知道到底该调哪个函数3、inlinea、内联函数的定义用 inline 修饰的的函数叫做内联函数编译时C编译器会在调用的地方展开内联函数这样调用内联函数就不需要建立栈帧了提高效率b、内联函数的使用下面我们来写一个简单的内联函数#include iostream using namespace std; inline int Add(int x, int y) { int ret x y; ret 1; return ret; } int main() { int ret Add(1, 2); cout ret endl; return 0; }通过调试来观察内联函数是否展开发现框选部分并没有call/ret指令即没有通过call指令进行调用直接将代码复制过来最后要注意inline不建议声明和定义分离到两个文件分离会导致连接错误因为inline被展开没有函数地址链接时会出现报错c、内联函数与宏函数的区别C设计内联函数的目的就是为了替代C语言的宏函数C语言的宏函数也会在预处理时替换展开但是宏函数很复杂并且由于直接替换不方便调试下面我们来简单实现一个加法宏函数//请问哪个宏函数是正确的 #define Add(int a,int b) return a b; #define Add(a,b) return a b; #define Add(a,b) (a b)答案是上述三个均是错误的宏函数宏函数的本质是进行替换首先来看第一个//#define Add(int a,int b) return a b; int ret Add(1, 2); //展开之后变成 int ret return 1 2;; //1.return不能出现在赋值表达式 //2.末尾有两个分号接着来看第二个//#define Add(a,b) a b; int ret Add(1, 2); //展开之后变成 int ret 1 2;; //此时影响不大结果正常 cout Add(1, 2) * 3 endl; //这样展开之后呢 //cout 1 2 * 3 ; endl; //显然1.没有括号 2.分号多余继续来看第三个//#define Add(a,b) (a b) cout Add(1, 2) endl; // - cout (1 2) endl; //此时没有问题 cout Add(1 2, 1 | 2) endl; // - cout (1 2 1 | 2) endl; //显然错误 的运算符优先级高于 ,导致计算顺序错误最后来看正确写法#define Add(a,b) ((a) (b));四、引用1、引用的概念和定义引用的基本概念引用是C中一种特殊的变量类型本质上是已存在变量的别名通过引用可以直接操作原变量无需拷贝数据引用在定义时必须初始化且一旦绑定到某个变量后无法更改指向。引用的定义语法引用的定义方式为在变量类型后加 符号类型匹配引用必须与原变量类型一致const引用除外。必须初始化定义时需直接绑定到一个已存在的变量。无独立内存引用不占用额外存储空间仅作为别名存在。我们尝试来使用引用#include iostream using namespace std; int main() { int a 1; int b a; int c a; int d a; cout a endl; cout b endl; cout c endl; cout d endl; return 0; }2、const引用a、引用const对象必须要用const来引用const引用也可以引用普通对象那么就会造成权限缩小的问题#include iostream using namespace std; int main() { const int a 1; //权限放大error C2440: “初始化”: 无法从“const int”转换为“int ” //int ra a; //正确引用 const int ra a; //error C3892: “ra”: 不能给常量赋值 //ra; //权限缩小 int b 10; const int rb b; //error C3892: “rb”: 不能给常量赋值 //rb; return 0; }b、临时对象是指编译器需要一个空间暂存表达式的求职结果时临时创建的一个未命名对象而在类型转换中就会产生临时对象而临时对象具有常性此时就需要使用常引用#include iostream using namespace std; int main() { int a 10; //常量值用常引用 const int x 20; //error C2440: “初始化”: 无法从“int”转换为“int ” //int ret a * 2; //临时对象用常引用 const int ret a * 2; double d 11.22; //error C2440: “初始化”: 无法从“double”转换为“int ” //int rd d; //类型转换会产生临时对象要用常引用 const int rd d; return 0; }3、指针和引用引用与指针的区别初始化要求引用必须初始化指针可以为空nullptr语法概念上引用是一个变量的别名不开空间指针是存储一个变量的地址要开空间可修改性引用绑定后不可更改指针可以重新指向其他地址访问语法引用直接使用变量名操作指针需解引用*ptr大小上sizoef中引用的大小取决于引用类型的大小但指针始终是地址空间所占字节个数安全性引用不存在“空引用”问题指针可能悬空五、空指针1、NULL在C语言中NULL实际上是一个宏在stddef.h文件中可以看到下面代码#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endifC中NULL被定义成0C语言中是一个类型为void *、值为零的空指针常量;我们来看下面代码#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 x) f((int*)0); f((int*)NULL);//调用f(int* ptr) //error C2665: “f”: 没有重载函数可以转换所有参数类型 //f((void*)NULL); return 0; }会发现即使传入NULL本想调用指针版本的却仍调用整数版本导致出现很多歧义最后由于C禁止void*隐式类型转换为其他指针类型因此会报错2、nullptr在C中为了避免函数调用歧义的问题引入了新的关键字 nullptr;nullptr可以转换成任意其他类型的指针因此使用nullptr可以避免指针类型转换的问题nullptr只能隐式的转换成指针类型而不能被转换成整数类型如果觉得有帮助可以关注 GitHub 项目持续更新GitHub - Stellen-z/DailyCode: pracetice · GitHub