文章目录
今日言:纵使失错,纵使低迷,我依旧前进着!
😋前言:
本节所述内容,是C++入门基础,基本上是解决C语言的一些不足之处。
一、关键字(C++11)💬
C++11中有73个关键字,其中有32个高亮的是C语言的关键字。
alignas | continue | friend | register | true |
---|---|---|---|---|
alignof | decltype | goto | reinterpret_cast | try |
asm | default | if | return | typedef |
auto | delete | inline | short | typeid |
bool | do | int | signed | typename |
break | double | long | sizeof | union |
case | dynamic_cast | mutable | static | unsigned |
catch | else | namespace | static_assert | using |
char | enum | new | static_cast | virtual |
char16_t | explicit | noexcept | struct | void |
char32_t | export | nullptr | switch | volatile |
class | extern | operator | template | wchar_t |
const | false | private | this | while |
constexpr | float | protected | thread_local | |
const_cast | for | public | throw |
对于关键字在此不进行细致讲解,后续深入学习后再进行介绍。
二、命名空间💬
在C语言中,我们经常会遇到这样的问题:在我的项目的另一个文件中,已经定义了一个叫a的变量,但可能文件过多导致忘记了,于是在新的文件中又创建了一个叫a的变量,此时编译器就会报错!告诉我们重定义了。这是命名冲突。命名冲突有两种情况:1.变量与库冲突;2.两个变量互相冲突。
为了解决命名冲突或名字污染的问题,C++新增了关键字:namespace
。其后跟上命名空间,可以定义一个域1。
2.1 命名空间的定义
构成一个命名空间域的方式:namespcace
+ 命名空间名称
+ { }
。
namespace myspace { int a = 1; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
命名空间域中可以定义变量/函数/类型等等。
2.2 命名空间的使用
2.2.1 使用方法
命名空间有三种使用的方法:
-
命名空间名称
+作用域限定符
。一个域就相当于一座高高的城墙,想要进入域中访问内部元素,就必须通过一扇城门——作用域限定符 ::(没错,就是两个冒号!)
作用域限定符 :: 的使用方法是,在 :: 前面写域的名字,在 :: 后面写想要访问的内容。样例请参考下面的代码。namespace myspace { int a = 1; int Add(int left, int right) { return left + right; } } int main() { printf("%d ", myspace::a); printf("%d\n", myspace::Add(1, 2)); return 0; }
-
using
+命名空间名称
+作用域限定符
+某个元素
+;
(使用关键字using
将命名空间中的某个成员展开)这种方法与刚才的城门不同,这里相当于让城内的某个元素出城来,这样就可以访问到它了。
namespace myspace { int a = 1; int Add(int left, int right) { return left + right; } } using myspace::a; int main() { printf("%d\n", a); return 0; }
-
using
+namespace
+命名空间名称
+;
(使用关键字using
将整个命名空间域展开)。这个方法更为直接,相当于把城墙给毁了,所有域内的元素都可以直接访问了。
namespace myspace { int a = 1; int Add(int left, int right) { return left + right; } } using namespace myspace; int main() { printf("%d\n", a); return 0; }
2.2.2 使用陷阱⚠️
通过C语言的学习,我们知道,当全局域和局部域都有一个叫做“a”的变量时,程序一般会先访问局部域的变量“a”,出了局部域后才会访问全局域的变量“a”。
-
如果全局域、局部域、命名空间域同时存在,程序会先访问谁呢?
不展开命名空间域,仅使用作用域限定符:
int a = 0;// 全局域 namespace myspace // 命名空间域 { int a = 1; } int main() { int a = 2;// 局部域 printf("%d\n", a); // 2,访问局部域 - 什么都不加,先访问局部域 printf("%d\n", ::a); // 0,访问全局域 - 只有::表示去访问全局域 printf("%d\n",myspace::a); // 1,访问命名空间域 - ::前指定了要去访问myspace这个命名空间域 return 0; }
结论:
- 如果没有域作用限定符,先访问局部域,再访问全局域,不会主动访问命名空间域。
- 只有**展开了命名空间域 ** 或者 用域作用限定符指定去访问命名空间域,程序才会去访问命名空间域。
-
程序不会主动去访问命名空间域。那如果展开了命名空间域之后,又会先访问谁呢?
展开命名空间域,同时使用作用域限定符:
int a = 0;// 全局域 namespace myspace // 命名空间域 { int a = 1; } using namespace myspace; int main() { int a = 2; printf("%d\n", a); // 2,访问局部域 - 优先访问局部域 printf("%d\n", ::a); // 0,访问全局域 - ::前啥都不加,还是去访问全局域 printf("%d\n", myspace::a); // 1,访问命名空间域 - 和不展开命名空间域时一样 return 0; }
结论:
- 局部域永远是最优先访问的。
- 在作用域限定符 :: 前面啥都不加的情况下,只能去访问全局域。就算展开了命名空间域,也只是去访问全局域。
- 展开命名空间域后,并没有把命名空间域中的内容混合到全局域中去。
-
局部域永远是优先的。如果不考虑局部域,展开命名空间域后,命名空间域和全局域又先访问谁呢?
展开命名空间域,不适用作用域限定符:
int a = 0; namespace myspace { int a = 1; } using namespace myspace; int main() { printf("%d\n", a); // 报错!!!a不明确。此时的a是命名空间域的还是全局域的,编译器也不知道。 return 0; }
结论:
- 当全局域和命名空间域并存的时候,如果展开了命名空间域并且两个域中有同名的变量,编译器会在全局域和命名空间域中都搜索这个变量,那么就会出现变量名不明确的错误!
- 结合前面的分析,就算展开了命名空间域,在作用域限定符 :: 前面啥都不加的情况下,也只能去访问全局域。说明了命名空间域展开后并不是去跟全局域合并。
而从这里的代码可见,展开了命名空间域,在没有作用域限定符的作用时,是会出现错误的!
总结:展开命名空间域是存在风险的,当代码量很大的时候,可能会出现我们意想不到的错误,因此,在项目中,最好不要轻易地展开命名空间,尤其是C++的命名空间std2。
2.3 命名空间的特点
-
1、命名空间之间可以互相嵌套。
可以防止某个命名空间太大,其内部发生命名冲突。
命名空间嵌套时访问方法:namespace N1 { namespace N2 { int a = 1; int b = 2; } } int main() { printf("%d\n", N1::N2::a); // 1 return 0; }
-
2、同一个工程中存在的多个相同名字的命名空间,编译器最后会合并成一个。(其实也不是合并,就是命名空间像是一张图纸,在不同的地方用同一个命名空间,就是往这个命名空间里加入越来越多的内容。这个图纸本身是没有空间的,因此也谈不上什么合并。)
例如:有两个文件Queue.h和Stack.h,它们中各自都有一个命名空间叫myspace,里面分别放了元素,只要这两个文件引了头文件,可以相互链接,那么它们就最终可以看作是同一个命名空间。因此只要在同一个项目里,我们就可以在不同的地方往同一个命名空间里插入新的内容。
三、输入输出💬
C++的输入、输出与C是不同的。
输入、输出示例:
#include<iostream> using namespace std; int main() { int a = 0; cin >> a; // cin是从键盘输入到a中 cout << a << endl; // cout是把a输出到控制台中;endl是在控制台中换行 return 0; }
在这里浅浅了解如何使用cin、cout即可,关于输入输出浮点数的精度、控制输入输出类型、ostream、istream、>>、<< 等知识后续再深入研究。
说明:
- 使用cout标准输出对象(控制台)、cin标准输入对象(键盘)以及符号endl时,必须包含头文件并使用命名空间std。
- << 是流插入运算符, >> 是流提取运算符
- cout和cin可以自动识别数据类型。(比C厉害!)
- std是C++标准库的命名空间,里面包含特别多的内容,因此我们使用时最好不要将其全部展开,像std::cout这样 或者 using std::cout 即可。当然,如果只是写一些日常学习的代码,不涉及很多代码、很多文件时,也可以直接using namespace std;
四、缺省参数💬
4.1 缺省参数的概念
概念:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该
形参的缺省值,否则使用指定的实参。
比如:
void Func(int a = 0) { cout << a << endl; } int main() { Func(); // 无实参,使用默认值,输出为0 Func(1); // 有实参,使用指定的参数,输出为1 return 0; }
4.2 缺省参数的分类
-
全缺省参数
函数参数a、b、c都缺省了。
void Func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << " "; cout << "b = " << b << " "; cout << "c = " << c << " "; cout << endl; } int main() { Func(); // 10 20 30 Func(1); // 1 20 30 Func(1, 2); // 1 2 30 Func(1, 2, 3); // 1 2 3 return 0; }
总结:
- 全缺省参数,若传入具体的实参,从左往右依次传入。传入1个则传至a,传入2个则传至a和b,传入3个则传至a、b、c。
- 传参的时候不能有空缺,比如 Func(1, , 3); 这种是万万不行的!!!
-
半缺省参数
a不缺省,b和c缺省。
void Func(int a, int b = 20, int c = 30) { cout << "a = " << a << " "; cout << "b = " << b << " "; cout << "c = " << c << " "; cout << endl; } int main() { Func(); // 错误!a没有设置默认参数,所以至少要传1个,给a。 Func(1); // 1 20 30 Func(1, 2); // 1 2 30 Func(1, 2, 3); // 1 2 3 return 0; }
总结:
- 半缺省参数指部分形参没有给默认值。(不是一半形参!)
- 要用半缺省参数,给默认值必须从右往左,这样是不行的:void Func(int a = 10, int b = 20, int c) !
注意:缺省参数的默认值必须是常量或者全局变量。
4.3 在声明处给缺省参数
对于缺省参数有一个问题:
如果声明和定义都给缺省参数,那当他们两个不一样时,该用哪一个呢?
C++为了避免出现这种情况,就规定了==缺省参数只能在声明处给,而不在定义处给。==
示例:
有三个文件:
Stack.h
/Stack.cpp
/Test.cpp
。Stack.h 中声明了一个函数Func,它有缺省参数。在 Stack.cpp 中给这个函数定义。在 Test.c 中使用这个函数。
那么,就必须在 Stack.h 中声明的时候写上缺省参数,然后在 Stack.cpp 中定义的时候不写缺省参数,这样,在 Test.c 中使用这个函数的时候才能使用缺省参数。// Stack.h void Func(int a, int b = 10); // Stack.cpp void Func(int a, int b) // 这里绝对不能写上缺省参数! { cout << a << endl; cout << b << endl; } // Test.cpp int main() { Func(1); Func(1, 2); return 0; }
为什么缺省参数只能在声明处给呢?
我们通过编译链接的过程来探究它的原理:
预处理时展开头文件,就会得到函数的声明。这时相当于得到一个口头的承诺:接下来的文件中有这个函数的定义,你拿去用就行了。在编译、汇编之后的链接时,我们的链接器才会去根据声明处的函数在编译时生产的符号表中的符号,在另外的文件中找到这个函数的定义的地址,这就相当于兑现承诺,到此时定义才出现。
我们通过编译链接的知识可知:函数的传参其实是在编译的时候完成的。使用函数时到底有没有传来实参,这在编译结束后就已经知道了,若没传,则把默认值给过去。编译结束时生成的汇编代码已经确定,不会再变化,后来又通过汇编生成机器码,通过链接生成可执行文件。
只有在编译时有了确定的参数,把参数都写入汇编代码中,转化成机器码之后,链接时才能去实现函数的操作。如果把缺省参数放在定义处,那时编译、汇编早已完成,机器码都出来了,里边却没有确定的参数值,此时找到函数的定义,参数才姗姗来迟,就晚了。因为链接器只会对目标文件进行处理,而参数都还没有被写进去呢!
五、函数重载💬
5.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似,数据类型不同的问题 ,返回值没有要求(名字和形参一样,就返回值不同,这不构成重载函数,原因见下面的编译链接原理分析)
5.2 函数重载的示例
-
参数类型不同
// 1、参数类型不同 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "double Add(double left, double right)" << endl; return left + right; } int main() { Add(10, 20); // int Add(int left, int right) Add(10.1, 20.2); // double Add(double left, double right) return 0; }
-
参数个数不同
// 2、参数个数不同 void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; } int main() { f(); // f() f(10); // f(int a) return 0; }
-
参数类型顺序不同(和形参的名字无关)
// 3、参数类型顺序不同(和形参的名字无关) void func(int a, char b) { cout << "f(int a,char b)" << endl; } void func(char b, int a) { cout << "f(char b, int a)" << endl; } int main() { func(10, 'a'); // f(int a,char b) func('a', 10); // f(char b, int a) return 0; }
-
涉及到缺省参数,有一个错误代码:
void f() { cout << "f()" << endl; } void f(int a = 0) { cout << "f(int a)" << endl; } int main() { f(); return 0; }
这两个函数确实是构成了函数重载,但是有一个问题:调用哪一个?
调用函数f()时,存在歧义,编译器不知道应该调用哪一个函数,因此这样写是错误的!
5.3 函数重载的原理
为什么C语言不支持函数重载,而C++就能支持了呢?
-
我们通过编译链接的底层原理来了解:
编译链接有多个过程:预编译、编译、汇编、链接。
预编译:头文件展开/宏替换/条件编译/去掉注释。
Stack.i Test.i ( * .h文件在.cpp文件中展开,分别生成这两个文件)
编译:语法分析,词法分析,语义分析,符号汇总,生成汇编代码。
Stack.s Test.s
汇编:形成符号表,汇编代码转换成二进制机器码。
Stack.o Test.o
链接:合并段表,合并符号表,符号表重定位,将多个*.o文件链接到一起。
xxx.exe / a.out ( *.o文件合并,生成可执行程序)在汇编的时候,会生成符号表,声明在* .h文件中的函数,会通过这个函数的符号去链接函数的地址(定义的地址)。C语言的符号表的规则是函数名叫啥符号就叫啥,而C++的符号是函数名加上形参构成的,因此形参类型不同、类型顺序不同、数量不同,它的符号都不同,所以C++就能够找到不同的函数地址,也因此在C++中是可以支持重载函数的。
-
总结:
-
所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
-
那么链接时,面对Add函数,链接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
-
由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,我们一般使用g++演示了这个修饰后的名字。
-
gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数名长度+函数名+类型首字母】。
-
Windows的VS中的命名十分复杂,但是道理都是类似的。
-
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为符号表中函数的符号与返回值无关,链接器没办法区分。
-
-
注意:
由于C和C++编译器对函数名字修饰规则的不同,在有些场景下可能就会出问题,比如:
- C++中调用C语言实现的静态库或者动态库,反之亦然
- 多人协同开发时,有些人擅长用C语言,有些人擅长用C++。在这种混合模式下开发,由于C和C++编译器对函数名字修饰规则不同,可能就会导致链接失败,在该种场景下,就需要使用extern “C”。在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。
具体操作见课件《C++入门》5.3
六、引用💬
6.1 引用的概念
**引用:**不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型
+&
+对象名
=引用实体
int a =10; int& ra = a;
实际上
ra
就是a
。
6.2 引用的特性
-
特性:
- 引用必须初始化,明确说明这个引用是谁的引用。
int a = 10; // int& ra; 该条语句编译时会出错!
- C++的引用的指向是不能改变的(别的语言可以),是谁的引用就是谁的,不能变成另外一个变量的引用。
int a = 0; int& b = a; int& c = b; int x = 10; c = x; // 这里的c=x是把x赋值给c,c永远是b的引用! // 在别的语言中引用是可以改变指向的。
- 引用类型必须和引用实体是同种类型的。
int a = 0; int& b = a; // 这里b必须和a的类型int一样
- 一个变量可以有多个引用。
void TestRef() { int a = 10; int& ra = a; int& rra = a; printf("%p %p %p\n", &a, &ra, &rra); } // 结果是同一块地址 // 说明a、ra、rra是同一个变量,只不过有多个名字。
6.3 引用的权限不能变大
-
引用的过程当中,权限不能够放大。
权限只可以平移、缩小。
左边的权限必须 <= 右边的权限 -
通过下面的代码示例可以学习:
// 不可以: const int a = 0; int&b = a; // b的权限比a大,不可以 // 可以: int a = 0; const int&b = a; // b的权限比a小,可以
int main() { int a = 0; const int& b = a; a++; // a++是可以的,a没有被const修饰,a的权限没有缩小。 b++; // b++是不行的,b被const修饰,b的权限缩小,但是不影响a,只是不能通过b修改a了,但a自己还是能自我修改。 cout << b << endl; return 0; } // 输出的结果b是1,其本质是:a与b是同一块空间,但是可以通过a改变a,不能通过b改变a。
double d = 1.11; int i = d; // 发生类型转换,产生一个int类型的临时变量,d赋值给临时变量,临时变量赋值给i。 // 所有的类型转换(包括隐式类型转换、强制类型转换、整型提升、截断等等)都会产生一个临时变量。 // 临时变量有一个特征:具有常性。就是类似于被const修饰。 int& ri = d; // 这是不行的,因为double到int发生类型转换,产生的临时变量具有常性,等号右边的权限小,权限不能放大,因此不行。 const int& ri = d; // 这是可以的,权限对等。
int func1() { static int x = 0; return x; } int& func2() { static int x = 0; return x; } int main() { // int& ret1 = func1(); // 这是不行的,函数返回的是临时变量,和上面一样,权限不能放大。 const int& ret1 = func1);// 这是可以的,ret1的权限没有变大。 int& ret2 = func2(); // 可以的,权限平移。 const int& ret2 = func2(); // 可以的,权限缩小。 return 0; }
为什么类型转换要产生临时变量?
因为原来的变量是不能被改变的,就要一个临时变量,在临时变量身上操作,把临时变量变成需要的类型,再赋值。
6.4 引用的使用场景
-
引用做参数(输出型参数3)
void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } int main() { int a = 1; int b = 2; Swap(a, b); return 0; }
-
引用做参数(减少拷贝提高效率 – 大对象/深拷贝类对象 – 以后会讲深拷贝)
因为引用不用开辟新的空间拷贝进去,引用和原本的变量用的是同一块空间。
引用可以避免传参的数据过大时开辟很多空间降低效率。
这一点指针也可以做到,因为指针也就是4个字节罢了。 -
引用作返回值(减少拷贝提高效率 – 大对象/深拷贝类对象;)
// 传值返回 int Count() { static int n = 0; n++; return n; } int main() { int ret = Count(); return 0; } // 这种情况,无论有没有static,函数中的n都会先生成临时变量(可能是寄存器),然后函数的栈帧销毁,最后把这个值赋值给ret // 传引用返回 - 正确样例 int& Count() { static int n = 0; // n存在静态区中,不会随着函数栈帧的销毁而销毁 n++; return n; } int main() { int ret = Count(); return 0; } // 这个情况下,就不会产生临时变量了,提高了效率。(有static是安全的,若去掉static就很危险!)
注意:
传引用返回的时候要使用static,让变量n变成静态的,在函数栈帧销毁、归还这块空间的使用权的时候,变量n的空间的使用权不会随之归还,因此可以得到返回值。
但若是不用static,那么返回的结果是不确定的(相当于非法访问),如果栈帧销毁后没有清理栈帧,那么结果侥幸是对的,但是若是清理了栈帧了,那么结果就是错的了。
// 传引用返回 - 错误样例: int& Count(int x) { int n = x; n++; return n; } int main() { int& ret = Count(10); cout << ret << endl; printf("\n"); cout << ret << endl; return 0; } // 两次ret的打印值不同,第一次是11,第二次是随机值。因为printf函数也使用了函数栈中,将ret的这块不属于我们的空间的内容改变了。其实两次ret的使用都是危险的、错误的。
总结:
- 基本任何场景都可以用引用传参
- 谨慎使用引用做返回值。出了函数作用域,对象不在了,就不能用引用返回,要是还在就可以用。
- 以下的场景可以用:有些malloc,全局变量,静态变量,常量值
-
引用作返回值 – 可以修改返回值(可以在函数外直接改变返回值,获取+修改)
比如在顺序表里,要让值为1的数据的值加5,要走n步:1.得到值为1的数据的地址;2.根据地址改变它的值。
而若是使用引用作返回值,那么只要找到值为1的数据并返回它,那么就可以直接在函数外对其进行修改。struct SeqList { int a[100]; size_t size; int& at(int pos) { assert(pos >= 0 && pos < 100); return a[pos]; } int& operator[](int pos) { assert(pos >= 0 && pos < 100); return a[pos]; } }; int main() { SeqList s; s.at(0) = 0; s.at(0)++; cout << s.at(0) << endl; s[1] = 2; cout << s[1] << endl; return 0; }
6.5 引用和指针的区别
-
引用和指针的区别:(面试常考点,不要背,建议理解)
-
引用概念上定义一个变量的别名,指针存储一个变量地址。
-
引用在定义时必须初始化,指针没有要求
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
-
没有NULL引用,但有NULL指针
-
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
-
有多级指针,但是没有多级引用
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
-
引用比指针使用起来相对更安全
-
-
指针开辟了空间,引用到底有没有开辟空间?
在语法上,引用只是别名,我们认为引用没有开辟新的空间。(指针会开辟新空间)
在底层上,底层汇编指令实现的角度看,引用是类似指针的方式实现的。也就是说还是要开辟空间的。
七、内联函数💬
7.1 内联函数概念
-
我们学过宏,宏有许多特点:
宏函数的优点:不需要建立栈帧,提高调用效率。可以复用,便于修改。
宏的缺点:容易出错。代码的可读性差。无法调试。 -
内联函数的概念
以
inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开(类似宏),
没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
只需要在函数前面加一个关键字inline
即可。
内联函数既有宏函数的优点,又避免了宏函数的缺点。
7.2 内联函数的特性
-
内联函数的特性:
inline
是一种以空间换时间的做法,在编译阶段会把函数体展开,而不是建立栈帧去调用函数。这样做的优点是少了调用的开销,提高运行效率,但缺点是:可能会使目标文件变大。
所以内联函数只适合于短小的、频繁使用的函数。否则会出现代码膨胀的问题。
比如:
Func函数编译后是50行指令,如果有10000个位置使用Func函数。
- Func不是内联函数,合计执行10000 + 50 行代码
- Func是内联函数,合计执行500000行代码 — 让它的可执行程序变大,假如写的是个软件,安装包就会很大。
所以不能把所有的函数都加上内联。
inline
仅仅是向编译器发出的一个请求,一个建议,编译器可以选择忽略掉这个请求,不同编译器关于inline
的实现机制可能不同。当出现以下情况时,编译器不会接受inline
的请求:
- 比较长的函数(函数内部的东西比较多)
- 递归函数
- 默认的debug模式时,
inline
不会起作用,可以设置一些属性,使其可以在debug模式起作用,就可以在汇编代码中看到了。
inline
最好不要声明和定义分离,分离会导致链接错误。
// 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 中被引用
原因:如果在声明处写了
inline
,定义处不写,那么执行函数的时候,会在*.h文件中找到它的声明,声明告诉程序它是个内联函数,需要展开来,但是因为此刻只有声明,没有具体的定义,因此展开失败,所以程序只能去符号表里找函数的地址。但是因为声明处写了inline
,导致函数根本就不会进入符号表,此时就根本找不到函数的地址,因此就出现链接错误! -
总结:
- 可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数,因为inline函数会在调用的地方展开,符号表中不会有inline函数的符号名,不存在链接冲突。
- 也可以直接把它的定义写在头文件,这样包含头文件的地方就都可以使用这个内联函数了。
八、auto关键字(C++11)💬
auto
能够自动识别右边的数据类型,把它给到左边的数据。
-
auto
可以替代名字很长的类型,使代码更具有可视性。 -
auto能自动识别很多种类型,对于内置类型,它都可以识别,但是对于自定义类型,auto无法识别!
其原因是auto的底层实现原理是:auto使用了很多个重载函数,把内置类型都重载了,但自定义类型可没有重载! -
int x = 10; auto* b = &x; // 这里auto*指定了b必须是个指针,右边必须是个地址。
九、范围for循环(C++11)💬
范围for循环是C++提供的语法糖。
所谓语法糖(Syntactic sugar),也译为糖衣语法,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。它可以给我们带来方便,是一种便捷的写法,编译器会帮我们做转换;而且可以提高开发编码的效率,在性能上也不会带来损失。
范围for循环适用于数组,它会依次取数组中的数据,赋值给e。自动迭代,自动判断结束。
int main()
{
int a[] = { 0,1,2,3,4,5,6,7,8,9,10 };
for (auto e : a) // 把每个元素赋值给e
{
cout << e << " ";
}
return 0;
}
int main()
{
int a[] = { 0,1,2,3,4,5,6,7,8,9,10 };
for (auto& e : a) // 要修改的话就要引用
{
e *= 2;
}
for (auto e : a)
{
cout << e << " ";
}
return 0;
}
但有的时候不能使用范围for循环,比如下面的代码:
void TestFor(int arr[])
{
for(auto e : a)
cout << e << endl;
}
因为c语言规定了,函数传参不能传数组,这里的arr其实是指针。不满足范围for循环的要求。
十、指针空值nullptr(C++11)💬
在C语言中,我们常用NULL
来表示一个空指针。NULL是一个宏,被替换成为了0。
以前使用没有问题,但是在C++中就可以出现问题了:
void f(int) //编译器对于形参而言,有没有接收都无所谓 { cout << "f(int)" << endl; } void f(int*) { cout << "f(int*)" << endl; } int main() { f(NULL); //f(int) f(nullptr); //f(int*) f(0); //f(int) return 0; }
C++中,#define NULL 0 , #define nullptr ((void*) 0)
原本在C语言中,NULL
与 (void*)0
是等价的,但是在C++中,NULL
却与 0
是等价的。在上面的场景中,f(NULL)与f(0)是等价的。所以在有些时候使用NULL,可能会得到与我们预期不符的结果,于是C++规定nullptr
与 (void*)0
等价,这样就可以避免一些问题了。
所以在C++中用空指针时,使用nullptr即可。
void TestFor(int arr[])
{
for(auto e : a)
cout << e << endl;
}
因为c语言规定了,函数传参不能传数组,这里的arr其实是指针。不满足范围for循环的要求。
十、指针空值nullptr(C++11)💬
在C语言中,我们常用NULL
来表示一个空指针。NULL是一个宏,被替换成为了0。
以前使用没有问题,但是在C++中就可以出现问题了:
void f(int) //编译器对于形参而言,有没有接收都无所谓 { cout << "f(int)" << endl; } void f(int*) { cout << "f(int*)" << endl; } int main() { f(NULL); //f(int) f(nullptr); //f(int*) f(0); //f(int) return 0; }
C++中,#define NULL 0 , #define nullptr ((void*) 0)
原本在C语言中,NULL
与 (void*)0
是等价的,但是在C++中,NULL
却与 0
是等价的。在上面的场景中,f(NULL)与f(0)是等价的。所以在有些时候使用NULL,可能会得到与我们预期不符的结果,于是C++规定nullptr
与 (void*)0
等价,这样就可以避免一些问题了。
所以在C++中用空指针时,使用nullptr即可。