C++----入门篇
引言
C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对C++学习有一定的帮助,本章节主要目标:
1. 补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。
2. 为后续类和对象学习打基础。
命名空间
在C++中,命名空间是一种封装名字的方式,用来解决命名冲突问题。它可以将一组名字(变量、函数、类型等)封装在一个域内,从而避免与其他名字发生冲突。
命名冲突
1.我们跟库冲突:当我们使用的名字与库中的名字相同时,会发生冲突。
2.我们互相之间冲突:在大型项目中,不同模块之间可能会使用相同的名字,导致冲突。
命名空间域与其他域
域分为类域、命名空间域、局部域、全局域。命名空间域是通过命名空间引入的一种新的域。
- ::为域作用限定符,用于访问不同域中的名字。如::a访问全局域的a,lynn::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 <cstdio>namespace 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 <cstdio>namespace 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语言的输入输出(需要包含<cstdio>)printf("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 <iostream>void 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 <iostream>void 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.h"void 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 <iostream>int main() {int a = 10;int &ref; // 错误:引用ref没有被初始化// ref = a; // 如果把初始化放在这里,也是不允许的,必须在声明时初始化// 正确的做法int &refToA = a; // 正确:在声明时初始化引用std::cout << "refToA: " << refToA << std::endl; // 输出:refToA: 10return 0;
}
2.1个变量可以有多个引用
#include <iostream>int 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 <iostream>int 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;//x=1 y=0return 0;
}
在数据结构中(链表的实现)的应用:
typedef struct ListNode{int val;
struct ListNode* next;
}LTNode,*PLTNode;void ListPushBack(PLTNode& phead,int x)
{//...//phead=newNode;//...
}
C++中的引用作为输出型参数具有以下几个优点:
-
语法更简洁:使用引用可以避免复杂的指针解引用操作,使代码更加清晰易读。
-
类型安全:引用在声明时必须被初始化,并且不能为空(尽管它可以引用一个空指针)。这有助于减少因未初始化指针或空指针解引用而导致的错误。
-
语义更清晰:引用明确表示了“别名”的关系,使得代码更易于理解和维护。
②提高效率
主要体现在针对大对象、深浅拷贝上。
- 大对象:在C++中,当处理大型对象或复杂数据结构(如大型类实例、动态数组、链表、树等)时,使用引用作为输出型参数可以显著提高效率。这是因为引用提供了对原始对象的直接访问,而无需复制整个对象。复制大型对象可能会非常耗时,并且会消耗大量内存,特别是在性能敏感的应用程序中。
- 深浅拷贝:后面章节会补充。
2.引用做返回值
①引用返回减少拷贝,提高效率
传值返回时,函数会创建一个临时变量(即返回值的拷贝),然后将其返回给调用者。这意味着对于大型对象或需要深拷贝的对象,传值返回可能会非常低效;引用返回则直接返回对象的引用(别名),避免了不必要的拷贝。这对于大型对象或需要频繁返回的对象特别有用。
注意:
- 函数返回的是局部变量的引用,那么这是危险的
因为局部变量在函数返回后会被销毁,所以返回的引用将指向一个已经不存在的对象,这会导致未定义行为:
在这里的代码中,打印出的b的值是不确定的:如果getRefWrong函数结束后,栈帧被销毁,但是没有清理栈帧,那么b的结果是正确的;如果getRefWrong函数结束后,栈帧被销毁,清理了栈帧,那么b的结果是随机值。
但是如果继续调用其他函数,我们再打印b就不会出现正确的结果了:
输出结果:
- 函数返回的是静态变量的引用,这是没问题的
②提高效率(针对大对象、深拷贝对象)
对于大型对象或需要深拷贝的对象,引用返回可以显著提高效率,因为它避免了对象的拷贝。
③在修改和获取返回值上也很方便
引用返回允许调用者直接修改返回的对象,而不需要通过额外的指针或引用来传递修改:
以前的做法:
#include<iostream>
#include<cassert>
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
#include<iostream>
#include<cassert>
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的地址。
#include<iostream>
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.基本使用
#include<map>
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.与迭代器一起使用
#include<iostream>
#include<string>
#include<vector>
using namespace std;int main()
{vector<int> v;//类型很长//vector<int>::iterator it = v.begin();//等价于auto it = v.begin();map<string, string> dict;//map<string, string>::iterator dit = dict.begin();等价于下面的语句:auto dit = dict.begin();return 0;
}
3.与范围 for 循环一起使用
#include<iostream>
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不是内联函数,合计起来有10000+50条指令(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)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
为了避免上述问题,通常建议将内联函数的声明和定义放在一起,通常是在头文件中。
指针空值nullptr(C++11)
NULL的问题
在C++11之前,空指针(即不指向任何有效内存地址的指针)通常通过NULL宏来表示。然而,这种表示方法存在一些问题,我们通过一个代码例子来看看会有什么问题:
我们发现,f(0)和f(NULL)调用的都是第一个f函数。这是因为NULL通常被定义为0或者((void*)0),这意味着它既可以被解释为整数0,也可以被解释为指针类型。
nullptr的提出
为了解决这些问题,C++11引入了一个新的关键字nullptr,用于表示空指针。nullptr的类型是std::nullptr_t,这是一个特殊的类型,专门用于表示空指针。对于上面的代码,如果我们往f函数传入nullptr,就会调用第二个f函数。
相关文章:

C++----入门篇
引言 C是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对C学习有一定的帮助,本章节主要目标: 1. 补充C语言语法的不足,以及C是如何对C语言…...

C语言程序设计P5-5【应用函数进行程序设计 | 第五节】—知识要点:变量的作用域和生存期
知识要点:变量的作用域和生存期 视频: 目录 一、任务分析 二、必备知识与理论 三、任务实施 一、任务分析 有一个一维数组,内放 10 个学生成绩,写一个函数,求出平均分、最高分和最低分。 任务要求用一个函数来完…...
用 Sass 模块化系统取代全局导入,消除 1.80.0 引入的 @import 弃用警告
目录 前言 问题 import 的缺陷 命名冲突 重复导入 模块系统 use 规则 forward 规则 实际修改 前言 最初,Sass 使用 import 规则通过单个全局命名空间加载其他文件,所有内置函数也可全局使用。由于模块系统(use 和 forward 规则&…...

安卓低功耗蓝牙BLE官方开发例程(JAVA)翻译注释版
官方原文链接 https://developer.android.com/develop/connectivity/bluetooth/ble/ble-overview?hlzh-cn 目录 低功耗蓝牙 基础知识 关键术语和概念 角色和职责 查找 BLE 设备 连接到 GATT 服务器 设置绑定服务 设置 BluetoothAdapter 连接到设备 声明 GATT 回…...
搭建fastapi项目
环境准备 # 创建项目目录 mkdir my_fastapi_project cd my_fastapi_project# 创建和激活虚拟环境 python -m venv venv .\venv\Scripts\activate安装必要的包 pip install fastapi uvicorn python-dotenv创建项目基本结构 my_fastapi_project/ │ .env # …...

Maven学习(Maven项目模块化。模块间“继承“机制。父(工程),子项目(模块)间聚合)
目录 一、Maven项目模块化? (1)基本介绍。 (2)汽车模块化生产再聚合组装。 (3)Maven项目模块化图解。 1、maven_parent。 2、maven_pojo。 3、maven_dao。 4、maven_service。 5、maven_web。 6…...
华为云云原生中间件DCS DMS 通过中国信通院与全球IPv6测试中心双重能力检测
近日,中国信息通信研究院(以下简称“中国信通院”)与全球IPv6测试中心相继宣布,华为云的分布式缓存服务(Distributed Cache Service,简称DCS)和分布式消息服务(Distributed Message …...

PostgreSQL中事件触发器Event Trigger
在PostgreSQL中,事件触发器(Event Trigger)是一种特殊的触发器类型,它允许你在特定的数据库系统事件发生时执行特定的操作。与普通的触发器不同,事件触发器并不与特定的表或视图相关联,而是与数据库级别的全…...
uni.request流式(Stream)请求,实现打印机效果
最近使用扣子 - 开发指南 (coze.cn)和智谱AI开放平台开发小程序AI导诊和用药对话指南。 开发的过程中也是走了不少坑,下面就来聊聊走了哪些坑。 坑1 :coze试了v2和v3的接口,两个接口请求还是有点差别的,v2拿到了botId和accessToken可以直接请求不需要做任何处理,v3还需要…...
canvas保存图片
需求:上面有几个按钮,其中有一个切换是图片 用v-if会导致图片加载慢 实现方法: 一进来就加载,通过监听元素显示,用于控制canvas的宽高,从而达到隐藏的效果 组件dowolad.vue <template><view …...
DNS到底有什么用?
举个例子,对于我们来说访问的域名是www.baidu.com,但是实际在计算机并不认识这个域名,计算机是需要通过IP地址去访问这个网站,所以呢?这个时候就需要一个dns解析器,来把这串域名转换为IP地址给计算机去访问…...

什么是CRM系统?CRM系统的功能、操作流程、生命周期
CRM系统作为企业管理和维护客户关系的重要工具,在商业活动中扮演着越来越重要的角色。今天,就让我们一起揭开它的神秘面纱,看看这个“幕后英雄”到底是怎么工作的。 什么是CRM系统? 首先,我们要了解什么是CRM。简单来…...

美畅物联丨JS播放器录像功能:从技术到应用的全面解析
畅联云平台的JS播放器是一款功能十分强大的视频汇聚平台播放工具,它已经具备众多实用功能,像实时播放、历史录像回放、云台控制、倍速播放、录像记录、音频播放、画面放大、全屏展示、截图捕捉等等。这些功能构建起了一个高效、灵活且用户友好的播放环境…...

我们来学mysql -- 事务并发之不可重复读(原理篇)
事务并发之不可重复读 题记不可重复读系列文章 题记 在《事务之概念》提到事务对应现实世界的状态转换,这个过程要满足4个特性这世界,真理只在大炮射程之类,通往和平的道路,非“常人”可以驾驭一个人生活按部就班,人多…...

ABAQUS进行焊接仿真分析(含子程序)
0 前言 焊接技术作为现代制造业中的重要连接工艺,广泛应用于汽车、船舶、航空航天、能源等多个行业。焊接接头的质量和性能直接影响到结构件的安全性、可靠性和使用寿命。因此,在焊接过程中如何有效预测和优化焊接过程中的热效应、应力变化以及材料变形等问题,成为了焊接研…...
BAPI_GOODSMVT_CREATE物料凭证增强字段
目的:增加字段LSMNG LSMEH的赋值 项目MSEG 的 BAPI 表增强结构 BAPI_TE_XMSEG 抬头MKPF 的 BAIP 表增强 BAPI_TE_XMKPF 1. 在结构BAPI_TE_XMSEG中appending structure附加结构 ZMSEG_001,增加字段LSMNG, LSMEH In The method IF_EX_MB_H…...

tomcat的优化和动静分离
tomcat的优化 1.tomcat的配置优化 2.操作系统的内核优化 注意:设置保存后,需要重新ssh连接才会看到配置更改的变化 vim /etc/security/limits.conf # 65535 为Linux系统最大打开文件数 * soft nproc 65535 * hard nproc 65535 * soft nofile 65535 *…...
[ShaderLab] 【Unity】【图像编程】理解 Unity Shader 的结构
在计算机图形学领域,开发者经常面临着管理着色器复杂性的挑战。正如大卫惠勒(David Wheeler)所说:“计算机科学中的任何问题都可以通过增加一层抽象来解决。” Unity 提供了这样一层抽象,即 ShaderLab,它通过组织和定义渲染过程的各个步骤,简化了编写着色器的过程。 什…...
vue的前端架构 介绍各自的优缺点
Vue.js 是一个用于构建用户界面的渐进式框架,可以根据项目的复杂性和需求选择不同的前端架构。以下是几种常见的 Vue 前端架构及其优缺点: 1. 单页应用 (SPA) 单页应用(Single Page Application,简称 SPA)是一种现代…...
可信AI与零知识证明的概念
可信AI 可信AI是指人工智能的设计、开发和部署遵循一系列原则和方法,以确保其行为和决策是可靠、可解释、公平、安全且符合人类价值观和社会利益的.以下是关于可信AI的举例说明、实现方式及主流方案: 举例说明 医疗诊断领域:一个可信AI的医疗诊断系统,不仅能够准确地识别…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...

如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

Linux 下 DMA 内存映射浅析
序 系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。 关于 dma_alloc_coherent 接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常…...