title: 入门
date: 2019-03-13 21:10:33
tags:
- Cpp
categories:
- Cpp
toc: true
从 C 到 C++
-
cout
中的 c 指的是 console。 -
endl
=> end line。 -
作用域限定符
::
相当于中文的
,表示作用域或所属关系。 -
extern "C"
有时候在 C++ 工程中可能需要将某些函数按照 C 的风格来编译,在代码前加 extern “C” ,意思是告诉编译器,将该函数按照C语言规则来编译。
extern “C” { something(); }
-
typeid(变量名).name
输出变量的类型。 -
bool
类型,true 为真,false 为假。任何基本类型都可以隐士转换为 bool 类型,非 0 即真,0 即假。
/* boolalpha -> 相当于一个开关,表示开,打印 true / false noboolalpha -> 表示关,关闭后打印 0 / 1 */ bool a = false; cout << a << " " << boolalpha << a << endl; a = 20; cout << noboolalpha << a << endl; a = *("abc"+3); // 0->a 1->b 2->c 3->\0 cout << boolalpha << a << endl; /* 这三种意思相同 a = *("abc"+3); a = "abc"[3]; a = 3["abc"]; */
命名空间(namespace)
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。
-
相同名字的 namespace 作用域相同,同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
-
命名空间的作用:避免名字冲突、划分逻辑单元,名字空间中的声明和定义可以分开。
-
名字空间可以嵌套,使用时候得一层一层的扒皮。
-
名字空间也可以赋值取别名,? 栗子:
namespace A1 { namespace A11 { namespace A12 { something(); } } } namespace A = A1::A11::A12; // A1::A11::A12::something(); 等价于 A::something();
-
using namespace A1;
就相当于”裸奔“ ,把 A1 中的东西暴露在当前作用域下。using namespace std;
也是一样,把 cout 暴露在全局下。风险:可能会出现命名冲突,一般还是带上
::
。
函数
缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
全缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30);
半缺省参数
void TestFunc(int a, int b = 10, int c = 20);
-
缺省参数必须从右开始设置。
/*ERROR*/ void fun(int a = 3,char b,char *c = "ahoj");
-
缺省参数不能在函数声明和定义中同时出现,建议声明时指定。如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
-
缺省值必须是常量或者全局变量。
-
C语言不支持(编译器不支持)。
哑元
只指定类型而不指定名称的函数,占着茅坑不拉屎。
? 栗子:
void ya(int,int b)
{
cout << b << endl;
}
int main()
{
ya(10,100);
return 0;
}
- 兼容老版本。
- 支持函数重载。
重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了”,后者是“谁也赢不了” 。函数重载:是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 / 类型 / 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
? 栗子:
void foo(){
cout << "void foo();" << endl;
}
void foo(int a){
cout << "void foo(int a);" << endl;
}
void foo(int a,int b){
cout << "void foo(int a,int b);" << endl;
}
void foo(int a,double b){
cout << "void foo(int a,double b);" << endl;
}
void foo(double a,int b){
cout << "void foo(double a,int b);" << endl;
}
int main()
{
foo();
foo(1);
foo(1 , 2);
foo(1 , 3.14);
foo(3.14 , 1);
}
- 同一作用域,函数名相同,参数表不同的函数。
- 参数表不同:
- 参数类型不同
- 参数个数不同
- 参数顺序不同
- 重载和形参名无关。
- 重载和返回类型无关。
- 不同作用域同名函数遵循就近原则。
重载的原理
nm a.out
,查看 C++ 编译器给看书取得名字:
00000001000010a0 T __Z3Maxii
0000000100000de0 T __Z3foodi
0000000100000cf0 T __Z3fooi
0000000100000d90 T __Z3fooid
0000000100000d40 T __Z3fooii
0000000100000b50 T __Z3foov
C++ 函数重载通过编译器改名实现。
名字修饰(Name Mangling)
在 C/C++ 中,一个程序要运行起来,需要经历:预处理、编译、汇编、链接。
Name Mangling 是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。
C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。
C++ 要支持函数重载、命名空间等,使得其修饰规则比较复杂,不同编译器在底层的实现方式可能都有差
异。
被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数
列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终
的名字中,就可保证名字在底层的全局唯一性。
? 文章:
引用(&)
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
李白 <=> 李太白 青莲居士 诗仙 ……
? 栗子:
void foo(int& a)
{
a++;
}
int main()
{
int a = 20;
int& b = a;
int& c = b;
cout << a << b << c << endl;
c = 10;
cout << a << b << c << endl;
cout << &a << " " << &b << " " << &c << " " << endl;
cout << "==========" << endl;
foo(a);
cout << a << endl;
return 0;
}
- 引用必须初始化且不能为空。
- 引用不能更换目标。
- 一个变量可以有多个引用。
- 引用不占用额外的内存。
- 引用类型必须和引用实体是同种类型的。
常引用
void TestConstRef()
{
const int a = 10;
// int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量 const int& b = 10;
double d = 12.34;
// int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d;
}
使用场景
-
做参数
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
-
做返回值
int& TestRefReturn(int& a) { a += 10; return a; }
⚠️
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
// 这段代码输出结果为一个随机值
如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型
返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。
传值和传引用(作为参数 / 作为返回值)在效率上的差距!
引用和指针的区别
-
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
-
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
// a.cpp int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
查看汇编代码:
保留编译过程中生成的临时文件:
g++ a.cpp -save-temps
其中
a.s
就是汇编文件,在 VS 里面可以 DEBUG 起来,直接看汇编代码,比较方便。 -
引用在定义时必须初始化,指针没有要求。
-
没有NULL引用,但有NULL指针。
-
在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数( 32 位平台下占 4
个字节) 。
-
引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小。
-
有多级指针,没有多级引用。
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
-
引用比指针使用起来相对更安全。
内联函数(inline)
以 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
以下是没有加 inline 的汇编代码。
在 Add 前加了 inline 后,在编译期间编译器会用函数体替换函数的调用。
-
inline 是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环 / 递归的函数不适宜使
用作为内联函数。 -
inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
-
inline 不建议声明和定义分离,分离会导致链接错误。因为 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 中被引用
auto(C++11)
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11 中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
? 栗子:
int TestAuto()
{
return 20;
}
auto b = 10;
auto c = 'a';
auto d = TestAuto();
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
⚠️
-
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类
型。因此 auto 并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将 auto 替换为变量实际的类型。 -
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &。
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; return 0; }
-
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
auto 不能推导的场景
-
auto 不能作为函数的参数。
void fun(auto a) {}
-
auto 不能直接用来声明数组。
-
为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。
-
auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有 lambda 表达式等进
行配合使用。
-
auto 不能定义类的非静态成员变量。
-
实例化模板时不能使用 auto 作为模板参数。
基于范围的 for 循环(C++11)
? 栗子:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
{
e *= 2;
}
for(auto e : array)
{
cout << e << " ";
}
return 0;
}
⚠️
-
与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。
-
for 循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
-
// 下面这段代码就有问题 void TestFor(int array[]) { for(auto& e : array) { cout<< e <<endl; } }
nullptr(C++11)
NULL 实际是一个宏,在传统的 C 头文件stddef.h
中:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
NULL 可能被定义为字面常量 0,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在
使用空值的指针时,都不可避免的会遇到一些麻烦,如下:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖。 在C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0。
为了避免混淆,C++11 提供了 nullptr ,即:nullptr 代表一个指针空值常量。nullptr 是有类型的,其类型为nullptr_t ,仅仅可以被隐式转化为指针类型,nullptr_t 被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
⚠️
- 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的。
- 在 C++11 中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。