天津企业如何建网站,工商注册需要准备什么材料,阿里云网站建设教学视频教程,中铁建设集团门户网站登陆引言
C是在C的基础之上#xff0c;容纳进去了面向对象编程思想#xff0c;并增加了许多有用的库#xff0c;以及编程范式等。熟悉C语言之后#xff0c;对C学习有一定的帮助#xff0c;本章节主要目标#xff1a;
1. 补充C语言语法的不足#xff0c;以及C是如何对C语言…引言
C是在C的基础之上容纳进去了面向对象编程思想并增加了许多有用的库以及编程范式等。熟悉C语言之后对C学习有一定的帮助本章节主要目标
1. 补充C语言语法的不足以及C是如何对C语言设计不合理的地方进行优化的比如作用域方面、IO方面、函数方面、指针方面、宏方面等。
2. 为后续类和对象学习打基础。
命名空间
在C中命名空间是一种封装名字的方式用来解决命名冲突问题。它可以将一组名字变量、函数、类型等封装在一个域内从而避免与其他名字发生冲突。
命名冲突
1.我们跟库冲突当我们使用的名字与库中的名字相同时会发生冲突。
2.我们互相之间冲突在大型项目中不同模块之间可能会使用相同的名字导致冲突。
命名空间域与其他域
域分为类域、命名空间域、局部域、全局域。命名空间域是通过命名空间引入的一种新的域。
::为域作用限定符用于访问不同域中的名字。如::a访问全局域的alynn::a访问命名空间lynn中的a。
命名空间的定义与使用
命名空间通过namespace关键字来定义可以包含变量、函数、类型等。
代码示例
#include cstdio// 定义一个命名空间lynn
namespace lynn {int a 10;void printA() {printf(lynn::a %d\n, a);}
}// 全局域中的a
int a 20;void printA() {printf(global a %d\n, a);
}int main() {// 访问全局域的aprintf(Global a: %d\n, ::a);// 访问命名空间bit中的aprintf(Namespace lynn a: %d\n, lynn::a);// 调用全局域的printA函数::printA();// 调用命名空间lynn中的printA函数lynn::printA();return 0;
}
输出 展开命名空间
using namespace lynn;
编译时会去命名空间lynn中搜索名字。
代码示例
#include cstdionamespace lynn {int a 10;void printA() {printf(lynn::a %d\n, a);}
}using namespace lynn;int main() {// 由于展开了命名空间lynn可以直接访问a和printAprintf(a %d\n, a);printA();return 0;
}
输出 展开了命名空间lynn后a相当于暴露在了全局。
搜索顺序
局部域 → 全局域 → 展开了的命名空间域→指定访问的命名空间域
命名空间的嵌套与合并
命名空间可以嵌套这允许我们创建更复杂的命名空间结构。这对于定义自己的库特别有用可以避免与C标准库或其他库发生冲突。
多个同名的命名空间会被合并这使得我们可以在不同的文件中定义同名的命名空间而不会发生冲突。
代码示例:
#include cstdionamespace outer {namespace inner {int x 100;}
}// 在另一个文件中我们可以继续扩展outer命名空间
namespace outer {int y 200;
}int main() {printf(outer::inner::x %d\n,outer::inner::x);printf(outer::y %d\n,outer::y);return 0;
}
输出: 输入和输出
在C中输入输出流是通过流插入运算符和流提取运算符来实现的。这两个运算符能够自动识别操作对象的类型并进行相应的处理。
流插入运算符用于将数据插入到输出流中如std::cout。流提取运算符用于从输入流中提取数据如std::cin。
在这里std::是命名空间前缀它指定了cout和cin是定义在std命名空间中的。这是为了避免命名冲突并确保我们使用的是标准库中的cout。
不建议在项目中展开std命名空间
容易命名冲突如果我们在项目中定义的跟std命名空间里面定义的重名就会报错。所以不建议在项目里展日常练习可以展开。
项目里面更推荐指定访问还可以把常用的展开例如using std::cout; using std::endl;
#include iostream
using std::cout;
using std::endl;int main() {double b 3.14;// 使用C的输入输出流cout b b endl;return 0;
}
精度丢失问题
流插入运算符可以自动识别插入数据的类型但在自动识别类型时可能会导致小数精度丢失因此在需要精确控制小数位数时推荐使用C语言风格的输出方式printf。
代码示例:
#include cstdio
#include iostream
using namespace std;int main() {int a 5;double b 3.1415926;// 使用C的输入输出流cout a a endl;cout b b endl;// 使用C语言的输入输出需要包含cstdioprintf(a %d\n, a);printf(b %.7f\n, b); // 精确到小数点后5位return 0;
}
输出 C的cout和C语言的printf
虽然C的cout提供了类型安全、易于扩展等优点但在某些情况下C语言的printf可能会更快因为它直接操作底层的输出缓冲区而cout则涉及更多的抽象和类型检查。
解决办法关闭同步流
std::cout默认与C语言的stdio库保持同步这意味着每次使用std::cout进行输出时程序都需要检查stdio库的缓冲区状态这会增加额外的开销。我们可以通过调用std::ios_base::sync_with_stdio(false);来关闭这种同步从而提高std::cout的效率。
#include iostream
using namespace std;int main() {ios_base::sync_with_stdio(false);cout Hello, World! endl;return 0;
}
缺省参数
在C中缺省参数允许我们在函数调用时省略某些参数这些被省略的参数将使用预先定义的默认值。缺省参数的使用可以大大提高代码的灵活性和可读性。以下是关于缺省参数的详细讲解和代码例子。
缺省参数的基本概念
缺省参数是在函数声明或定义中为某些参数指定默认值。当函数调用时如果没有提供这些参数的值那么就会使用这些默认值。
全缺省参数
全缺省参数指的是函数的所有参数都有默认值。在调用这样的函数时可以选择不提供任何参数函数将使用所有参数的默认值。
代码例子
#include iostreamvoid printValues(int a 1, int b 2, int c 3) {std::cout a: a , b: b , c: c std::endl;
}int main() {printValues(); // 使用所有缺省值a: 1, b: 2, c: 3printValues(4); // a: 4, b: 2, c: 3printValues(4, 5); // a: 4, b: 5, c: 3printValues(4, 5, 6); // a: 4, b: 5, c: 6return 0;
}
半缺省参数
半缺省参数指的是函数的部分参数有默认值而其他参数没有。在调用这样的函数时必须提供没有默认值的参数而可以选择省略有默认值的参数。
注意缺省参数必须从右往左给出不能隔着给。
代码例子
#include iostreamvoid printValues(int a, int b 2, int c 3) {std::cout a: a , b: b , c: c std::endl;
}int main() {printValues(1); // a: 1, b: 2, c: 3printValues(1, 4); // a: 1, b: 4, c: 3printValues(1, 4, 5); // a: 1, b: 4, c: 5// printValues(); // 错误缺少非缺省参数areturn 0;
}
声明和定义中的缺省参数
在C中函数的声明和定义通常分开进行特别是在使用头文件.h和源文件.cpp的情况下。需要注意的是缺省参数只能在函数的声明通常在头文件中中给出而不能在函数的定义通常在源文件中中再次给出。
代码例子
头文件
#ifndef EXAMPLE_H
#define EXAMPLE_Hvoid printValues(int a 1, int b 2, int c 3);#endif // EXAMPLE_H
源文件
#include iostream
#include example.hvoid printValues(int a, int b, int c) {std::cout a: a , b: b , c: c std::endl;
}int main() {printValues(); // 使用缺省值a: 1, b: 2, c: 3return 0;
}
在这个例子中缺省参数是在头文件的函数声明中给出的而在源文件的函数定义中没有再次给出。这是正确的做法因为如果在定义中也给出缺省参数将会导致编译错误。
函数重载
函数重载的概念
函数重载是指在同一作用域内允许创建多个函数它们具有相同的函数名但具有不同的参数列表参数个数、参数类型或参数类型顺序不同。编译器会根据调用时提供的参数来匹配最合适的函数进行调用。函数重载提高了代码的复用性和可读性。
函数重载的不同形式
1.参数类型不同
当函数名相同但参数类型不同时可以构成函数重载。例如
void print(int i) {cout Integer: i endl;
}void print(double d) {cout Double: d endl;
}
调用print(5)会匹配到第一个函数而print(5.5)会匹配到第二个函数。
注意函数名相同参数不同返回值不同不构成函数重载。
void func(int a int b) {// 这是一个函数
}int func(int a) { // 这是一个错误因为与上面的函数返回值类型不同不构成重载return a;
}
2.参数个数不同
参数个数不同也是函数重载的一种形式。例如
void func() {cout No parameters endl;
}void func(int a) {cout One parameter: a endl;
}
调用func()会匹配到第一个函数而func(10)会匹配到第二个函数。
3.参数类型顺序不同
参数类型的顺序不同也可以构成函数重载。例如
void mix(int a, double b) {cout Int and Double: a , b endl;
}void mix(double a, int b) {cout Double and Int: a , b endl;
}
调用mix(1, 2.0)会匹配到第一个函数而mix(2.0, 1)会匹配到第二个函数。
可能发生歧义的情况
在C中如果一个函数有默认参数而另一个函数没有参数且函数名相同这会导致调用时的歧义。例如
void func() {cout No parameters endl;
}void func(int a 0) {cout One parameter with default: a endl;
}
在这种情况下调用func()时编译器无法确定应该调用哪个函数因为两个函数都可以接受没有参数的情况。因此这种重载是不允许的会导致编译错误。
C语言与C在函数重载上的区别及C的支持原理
前置知识
一、GCC与C语言的关系 GCC定义GCC是一个开源的编译器套件最初是为C语言设计的后来扩展支持了多种编程语言。
C语言编译GCC作为C语言的主要编译器之一负责将C语言源代码编译成机器码使其能够在计算机上运行。
编译过程GCC编译C语言代码的过程包括预处理、编译、汇编和链接等阶段。
二、G与C的关系
G定义G是GCC套件中的一个编译器专门用于编译C代码。
C编译G能够处理C的复杂语法和特性包括类、对象、继承、多态等将C源代码编译成机器码。
函数重载的原理讲解
C语言不支持函数重载
C语言的编译过程相对简单函数名在编译后直接作为符号使用没有额外的修饰。由于这种设计C语言无法区分同名但参数不同的函数因此不支持函数重载。GCC作为C语言的编译器遵循C语言的语法和规则因此也不会在编译过程中为C语言提供函数重载的支持。
C支持函数重载
为了实现函数重载C采用了“名字修饰”或“名字改编”的技术。G作为C的编译器实现了C的名字修饰规则为每个重载的函数生成一个独特的内部名字。在编译过程中G会根据函数的参数类型、参数个数和参数类型顺序等信息对函数名进行修饰确保编译器和链接器能够区分开不同的函数。
引用 引用的特性
1.引用必须初始化
#include iostreamint main() {int a 10;int ref; // 错误引用ref没有被初始化// ref a; // 如果把初始化放在这里也是不允许的必须在声明时初始化// 正确的做法int refToA a; // 正确在声明时初始化引用std::cout refToA: refToA std::endl; // 输出refToA: 10return 0;
}
2.1个变量可以有多个引用
#include iostreamint main() {int a 10;int ref1 a;int ref2 a; // 合法ref1和ref2都是a的引用ref1 20;std::cout a: a , ref1: ref1 , ref2: ref2 std::endl;// 输出a: 20, ref1: 20, ref2: 20// 说明ref1和ref2都是指向a的引用return 0;
}
3.当一个引用一旦引用一个实体就不能引用其他实体。
#include iostreamint main() {int a 10;int b 20;int ref a; // ref初始化为a的引用// ref b; // 错误这里尝试让ref引用b这是不允许的return 0;
}
引用的应用场景
1.引用做参数
①做输出型参数
通过引用在函数中交换int变量的数据
#include iostream
using namespace std;void Swap(int a, int b)
{int tmp a;a b;b tmp;
}
int main()
{int x 0, y 1;Swap(x, y);cout x x y y endl;//x1 y0return 0;
}
在数据结构中链表的实现的应用
typedef struct ListNode{int val;
struct ListNode* next;
}LTNode,*PLTNode;void ListPushBack(PLTNode phead,int x)
{//...//pheadnewNode;//...
}
C中的引用作为输出型参数具有以下几个优点 语法更简洁使用引用可以避免复杂的指针解引用操作使代码更加清晰易读。 类型安全引用在声明时必须被初始化并且不能为空尽管它可以引用一个空指针。这有助于减少因未初始化指针或空指针解引用而导致的错误。 语义更清晰引用明确表示了“别名”的关系使得代码更易于理解和维护。
②提高效率
主要体现在针对大对象、深浅拷贝上。
大对象在C中当处理大型对象或复杂数据结构如大型类实例、动态数组、链表、树等时使用引用作为输出型参数可以显著提高效率。这是因为引用提供了对原始对象的直接访问而无需复制整个对象。复制大型对象可能会非常耗时并且会消耗大量内存特别是在性能敏感的应用程序中。深浅拷贝后面章节会补充。
2.引用做返回值
①引用返回减少拷贝提高效率
传值返回时函数会创建一个临时变量即返回值的拷贝然后将其返回给调用者。这意味着对于大型对象或需要深拷贝的对象传值返回可能会非常低效引用返回则直接返回对象的引用别名避免了不必要的拷贝。这对于大型对象或需要频繁返回的对象特别有用。
注意
函数返回的是局部变量的引用那么这是危险的
因为局部变量在函数返回后会被销毁所以返回的引用将指向一个已经不存在的对象这会导致未定义行为 在这里的代码中打印出的b的值是不确定的如果getRefWrong函数结束后栈帧被销毁但是没有清理栈帧那么b的结果是正确的如果getRefWrong函数结束后栈帧被销毁清理了栈帧那么b的结果是随机值。
但是如果继续调用其他函数我们再打印b就不会出现正确的结果了 输出结果 函数返回的是静态变量的引用这是没问题的 ②提高效率针对大对象、深拷贝对象
对于大型对象或需要深拷贝的对象引用返回可以显著提高效率因为它避免了对象的拷贝。
③在修改和获取返回值上也很方便
引用返回允许调用者直接修改返回的对象而不需要通过额外的指针或引用来传递修改
以前的做法
#includeiostream
#includecassert
using namespace std;struct SeqList
{int a[100];size_t size;
};int SLGet(SeqList* ps, int pos)
{assert(pos 100 pos 0);return ps-a[pos];
}void SLModify(SeqList* ps, int pos, int x)
{assert(pos 100 pos 0);ps-a[pos] x;
}int main()
{SeqList s;SLModify(s, 0, 1);//获取第0个位置的值int ret1 SLGet(s, 0);cout ret1 endl;// 对第0个位的值5SLModify(s, 0, ret1 5);ret1 SLGet(s, 0);cout ret1 endl;return 0;
}
有引用的做法
#define _CRT_SECURE_NO_EARNINGS 1
#includeiostream
#includecassert
using namespace std;struct SeqList
{int a[100];size_t size;
};int SLGet(SeqList* ps, int pos)
{assert(pos 100 pos 0);return ps-a[pos];
}void SLModify(SeqList* ps, int pos, int x)
{assert(pos 100 pos 0);ps-a[pos] x;
}int SLAt(SeqList s, int pos)
{assert(pos 100 pos 0);return s.a[pos];
}int main()
{SeqList s;SLAt(s, 0) 1;//获取第0个位置的值cout SLAt(s, 0) endl;//对第0个位的值5SLAt(s, 0) 5;cout SLAt(s, 0) endl;return 0;
}
常引用
前置知识临时变量具有常性
在C编程中临时变量是在表达式计算或函数返回时自动生成的用以存储短暂的数据结果。这些临时变量具备常性即它们是不可被修改的。原因在于临时变量往往没有具体的名称因此无法通过名称来对其进行访问或更改。此外即便临时变量以某种方式变得可访问例如通过引用与其绑定编译器也会坚守其常性从而避免对其造成意外的修改。
在数据类型转换或函数返回值的场景中临时变量尤为常见
①当我们将一个对象转换为与之兼容但类型不同的新对象时可能会生成一个临时变量来承载转换后的数据。 这里发生了隐式类型转换创建了int的临时变量
②当函数返回一个对象时特别是通过值传递的方式也可能会产生一个临时变量来保存这个返回值。 func1函数返回x时会生成一个int的临时变量存储返回值x。
权限不能放大但是可以平移或者缩小
引用的过程中权限不能放大但是权限可以平移或者缩小。
权限不能放大这一原则确保了常量对象const对象的不可变性。换句话说我们不能从一个常量对象创建一个非常量引用因为这将破坏常量对象的不可修改性。
权限可以平移或缩小平移意味着保持原有的常量性缩小则表示我们主动放弃对原本可变对象的修改权限选择通过常量引用来访问它。
我们会通过几个例子来帮助理解
①在const变量中
#include iostream
using namespace std;int main() {const int constVar 10; // 定义一个常量变量// 错误做法权限被放大了尝试从常量变量创建一个非常量引用int nonConstRef constVar; // 这行代码会编译错误因为不能从const int创建int// 正确做法权限可以平移从常量变量创建一个常量引用const int constRef constVar;// 通过常量引用访问常量变量的值cout constVar: constRef endl; // 输出constVar: 10// 错误做法权限被放大了尝试通过常量引用来修改常量变量的值constRef 20; // 这行代码会编译错误因为constRef是一个常量引用不能修改它所引用的值return 0;
}
②在数据类型转换中 ③在函数返回值中也要注意
引用和指针的区别
语法角度
语法层面上引用没有开空间是对a取别名。而指针开了空间来存储a的地址。
#includeiostream
using namespace std;int main()
{int a 10;//语法层面不开空间是对a取别名int ra a;ra 20;//语法层面开空间存储a的地址int* pa a;*pa 30;return 0;
}
底层汇编指令的角度
但是从底层汇编指令的角度看引用是类似指针的方式实现的。 其他不同
不建议背建议去理解
引用概念上定义一个变量的别名指针存储一个变量地址。引用在定义时必须初始化指针没有要求。引用在初始化时引用一个实体后就不能再引用其他实体而指针可以在任何时候指向任何一个同类型实体。没有NULL引用但有NULL指针在sizeof中含义不同引用结果为引用类型的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)。引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小。有多级指针但是没有多级引用。访问实体方式不同指针需要显式解引用引用编译器自己处理。引用比指针使用起来相对更安全。
总结
基本任何场景都可以用引用传参数。但是要谨慎用引用做返回。出了函数作用域对象不在了就不能用引用返回对象还在就可以用引用返回静态变量、malloc。
auto关键字
auto关键字能自动推断出变量的类型让我们写代码更轻松。
作用
1.类型自动推断编译器会自动根据初始化表达式来推断变量的类型。
2.简化代码当类型名很长或者复杂时用auto可以让代码更简洁。
3.避免类型错误有时候类型写错了可能不容易发现用auto就能减少这种错误。
代码例子
1.基本使用
#includemap
using namespace std;int main()
{int a 0;int b a;auto c a; // 根据右边的表达式自动推导c的类型auto d 1 1.11; // 根据右边的表达式自动推导d的类型cout typeid(c).name() endl;//检测变量c的数据类型cout typeid(d).name() endl;//检测变量d的数据类型return 0;
}
2.与迭代器一起使用
#includeiostream
#includestring
#includevector
using namespace std;int main()
{vectorint v;//类型很长//vectorint::iterator it v.begin();//等价于auto it v.begin();mapstring, string dict;//mapstring, string::iterator dit dict.begin();等价于下面的语句auto dit dict.begin();return 0;
}
3.与范围 for 循环一起使用
#includeiostream
using namespace std;int main()
{int arr[] { 1, 2, 3, 4, 5 };for (int i 0; i sizeof(arr) / sizeof(int); i)arr[i] * 2;for (int* p arr; p arr sizeof(arr) / sizeof(arr[0]); p)cout *p ;cout endl;// 范围for 语法糖// 依次取数组中数据赋值给x自动迭代自动判断结束for (auto x : arr)//等价于 for (int x : arr){cout x ;}cout endl;// 修改数据// auto关键字让编译器自动推断e的类型表示e是对容器中元素的引用。// 这意味着通过e修改的值会反映到原始容器arr中。// 如果只用auto e则e会是容器元素的一个拷贝对e的修改不会影响到原始容器中的元素。for (auto e : arr){e * 2;}// 分别打印出arr里的数据都*2后的结果for (auto e : arr){cout e ;}cout endl;return 0;
}
auto关键字不能应用的场景
1.auto不能作为函数的参数
2.auto不能直接用来声明数组
内联函数
内联函数提出的背景
C语言宏函数的优点不需要建立栈帧提高效率
宏函数在编译时会被直接替换为它们的代码体因此不涉及栈帧的建立和销毁。宏函数的这种特性使得它们可以比普通函数更快地执行特别是在频繁调用的小函数或性能敏感的场景中。
宏函数的弊端容易出错可读性差不能调试。
容易出错宏函数的参数如果是表达式可能在宏展开时产生意外的副作用。可读性差复杂的宏函数可能使代码难以理解特别是当宏中包含多个语句或嵌套宏时。不能调试由于宏函数在预处理阶段被替换调试时无法看到宏的调用只能看到展开后的代码。
C内联函数的提出
为了解决C语言宏函数的不足同时保留其不需要建立栈帧的优势C引入了内联函数inline函数。内联函数是一种提示编译器将函数体直接插入到每个调用该函数的地方而不是像普通函数那样进行调用。
inline int add(int x, int y) {return x y;
}int main() {int a 5, b 10;int result add(a, b); // 编译器可能会将add函数的体直接插入到这里return 0;
}
内联函数使用时需要注意的点
1.适用于内容短小、被频繁调用的函数。内容多的函数不适合当内联函数容易出现代码膨胀增加程序的内存占用。
我们来举个例子假如这里有一个Func函数编译好后是有50行的指令而在一个项目中有10000个位置调用了Func如果Func不是内联函数合计起来有1000050条指令10000个call Func调用函数的指令和50条函数自身的指令如果Func是内联函数合计起来有10000×50条指令因为每一次调用都要把内联函数展开。最终会导致可执行程序变占用的内存变大安装包变大。
inline对于编译器来说仅仅只是一个建议最终是否能成为inline编译器自己决定。比如比较长的函数、递归函数就算加了inline也不会成为内联函数。
2.默认Debug模式下inline不会成为内联函数不然会不方便调试。
如果需要让inline在Debug模式下成为内联函数需要进行下面的设置 3.inline不建议声明和定义分离分离会导致链接错误。
在C/C中通常建议将函数的声明放在头文件中而将函数的定义放在源文件中。这样做的好处是可以提高代码的可维护性和可重用性。然而对于内联函数来说这种做法可能会导致问题。当内联函数的声明和定义分离时如果多个源文件都包含了该内联函数的声明并且都尝试使用该内联函数那么在链接阶段就可能会遇到链接错误。原因内联函数没有独立的函数地址内联函数在编译时会被展开因此它不会像普通函数那样在生成的二进制文件中占有一个独立的地址。如果某个源文件中的代码尝试获取该内联函数的地址例如通过函数指针那么编译器将无法找到这个地址因为内联函数根本就没有生成独立的函数体。 // F.h
#include iostream
using namespace std;
inline void f(int i);// F.cpp
#include F.h
void f(int i)
{cout i endl;
}// main.cpp
#include F.h
int main()
{f(10);return 0;
}// 链接错误main.obj : error LNK2019: 无法解析的外部符号 void __cdecl
f(int) (?fYAXHZ)该符号在函数 _main 中被引用
为了避免上述问题通常建议将内联函数的声明和定义放在一起通常是在头文件中。
指针空值nullptrC11
NULL的问题
在C11之前空指针即不指向任何有效内存地址的指针通常通过NULL宏来表示。然而这种表示方法存在一些问题我们通过一个代码例子来看看会有什么问题 我们发现f(0)和f(NULL)调用的都是第一个f函数。这是因为NULL通常被定义为0或者((void*)0)这意味着它既可以被解释为整数0也可以被解释为指针类型。 nullptr的提出
为了解决这些问题C11引入了一个新的关键字nullptr用于表示空指针。nullptr的类型是std::nullptr_t这是一个特殊的类型专门用于表示空指针。对于上面的代码如果我们往f函数传入nullptr就会调用第二个f函数。