C++入门
1.命名空间
在c/c++中,变量,函数和后面要学到的类都是大量存在的,这些变量,函数和类的名称都将存在与全局作用域中,可能会导致很多冲突,使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
1.1 命名空间的定义
定义命名空间,需要用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间空间的成员。
//1.普通的命名空间 namespace N1//N1为命名空间的名称 { //命名空间中的内容,既可以是变量,也可以是定义函数 int a; int Add(int left, int right) { return left + right; } } //2.命名空间可以嵌套 namespace N2 { int a; int b; int Add(int left, int rght) { return left + right; } namespace N3 { int c; int d; int Sub(int left, int right) { return left - right; } } } //3.同一个工程中允许存在多个相同名称的命名空间 //编译器最后会合成同一个命名空间中 namespace N1 { int Mul(int left, int right) { return left * right; } }
【注意】:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
1.2 命名空间中使用
命名空间中成员该如何使用呢?比如:
namespace N { int a = 10; int b = 20; int Add(int left, int right) { return left + right; } int Sub(int left, int right) { return left - right; } } int main() { cout << a << endl;//该语句编译出错,无法识别a return 0; }
命名空间的使用有三种方式:
加命名空间名称及作用域限定符
using N::b; int main() { printf("%d\n", N::a); return 0; }
使用using将命名空间中成员引入
using N::b; int main() { cout << N::a << endl; cout << b << endl; return 0; }
使用using namespace命名空间名称引用
using namespace N; int main() { cout << N::a << endl; cout << b << endl; Add(10, 20); return 0; }
2.C++输入&&输出
#include <iostream> using namespace std; int main() { cout << "hello world" << endl; return 0; }
说明:
使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含<iostream>头文件以及std标准命名空间。
使用c++输出输出更方便,不需增加数据格式控制,比如:整形--%d, 字符--%c
#include <iostream> using namespace std; int main() { int a; double b; char c; cin >> a; cin >> b >> c; cout << a << endl; cout << b << " " << endl; return 0; }
3. 缺省参数
3.1 缺省参数的概念
缺省参数就是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
void TestFunc(int a = 3) { cout << a << endl; } int main() { TestFunc();//没有传参时,使用参数的默认值 TestFunc(5);//传参时,使用指定的实参 }
3.2 缺省参数的分类
全缺省参数
void TestFunc(int a = 1, int b = 2, int c = 3) { cout << "全缺省参数" << endl; }
半缺省参数
void TestFunc(int a, int b = 2, int c = 3) { cout << "半缺省参数" << endl; }
【注意】:
半缺省参数必从右向左依次来给出,不能间隔这给
缺省参数不能在函数声明和定义同时出现
//test.h void TestFunc(int a = 1, int b =2) { } //test.cpp void TestFunc(int a = 2, int b = 1) { } //注意:如果声明和定义位置同时出现,恰巧两个位置提供的值不同,那么编译器就无法确定到底要用那个缺省值
缺省值必须是全局变量
C语言不支持缺省(编译器不支持)
4.函数重载
4.1函数重载的概念
函数重载:是函数的一种特殊情况,c++允许在同一个作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能相似,数据类型不同的问题。
int Add(int left, int right) { return left + right; } double Add(double left, double right) { return left + right; } float Add(float left, double right) { return left + right; }
【注意】:参数顺序不同也可以重载
#include <iostream> using namespace std; class Test { public: void TestFunc(int a, float b) { cout << "int next float" << endl; } void TestFunc(float a, int b) { cout << "float next int" << endl; } private: int _a; }; int main() { Test a; a.TestFunc(2, 2.1); a.TestFunc(2.1, 2); return 0; }
输出结果: int next float float next int
结论:对于函数重载,只需要函数的参数个数不同,参数类型不同,参数顺序不同就可以进行重载,仅仅只有返回值不同是不够的。
【面试题】:对于函数仅有返回值不同,这两个函数可以重载吗?
答: 不可以,由于函数的名字修饰而决定的。
对于c++编译器在底层使用的不是函数的名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰过的名字中包含了:函数的名字和参数类型,这就是为什么函数重载中几个同名函数要求参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的的名字中,就可以保证在底层的唯一性。
所以对于函数是否可以重载主要与参数列表有关,与返回值没有关系。返回值只是在代码运行阶段进行检查,而参数在编译期间,就要进行函数名字修饰
4.2 名字修饰
在c/c++中,一个程序要运行起来,需要经历以下几个阶段:预处理,编译,汇编,连接
Name Mangling是一种在编译过程中,将函数,变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数听过某种算法,重新修饰为一个全局的唯一名称。
C语言的名字修饰规则十分的简单,只是在函数名字前面添加了下划线,比如:对于以下代码在最后链接是就会出错。
int Add(int left, int right); int main() { cout << Add(3, 4) << endl; return 0; }
编译器报错:error LNK2019:无法解析的外部符号_Add,该符号在函数 _main中引用。
由于C++要支持函数重载,命名空间等,使得其修饰规则比较复杂,不同的编译器在底层的实现方式可能都有不同。
对于c++编译器在底层使用的不是函数的名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰过的名字中包含了:函数的名字和参数类型,这就是为什么函数重载中几个同名函数要求参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的的名字中,就可以保证在底层的唯一性。
【linux下函数名字的修饰规则】:
对于c++,函数名字考虑函数的名字和参数
对于c,只考虑函数的名字(由于c语言不支持函数重载,所以修饰规则简单)
//.cpp int Add(int leftm int right) { } //修饰:Add(int , int) //只有函数名字和参数 //.c int Add(int left, int right) { } //修饰:Add //只有函数名字
【C++函数名字修饰规则】:
函数的名字,参数类型和返回值,还有名称空间都被加入到了修饰后的名称。
5. extern “C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数面前加extern "C",意思就是告诉编译器,将该函数按照C语言规则来进行编译。
extern "C" int Add(int left, int right); int main() { Add(2, 3); return 0; }
链接时报错:error LNK2019:无法解析的外部符号_Add,该符号在**函数 _main中引用
【面试题】:
下面两个函数可以重载吗?有问题吗?或者什么情况下会出问题?
#include <iostream> using namespace std; class Test { public: void TestFunc(int a = 9) { cout << "haveParameter:" << a << endl; } void TestFunc(int a) { cout << "noHaveParameter" << a << endl; } private: int _a; }; int main() { Test t; t.TestFunc(10); return 0; }
答:这两个函数不可以重载。
非缺省的函数,函数参数不能被重新声明
C语言为什么不支持重载呢?
答:这是在编译的时候对于函数的名字的修饰规则不同。
对于c语言函数名字的修饰规则简单,只要函数名相同修饰出来的函数名字依旧相同与函数的参数类型,参数个数以及名称空间无关,而对于C++的函数名字修饰规则就较为复杂,对于一个函数名字的修饰既与函数名字有关并且还与函数的参数个数,参数类型,参数顺序以及名称空间有关。
所以C语言不支持重载。
C++中函数重载底层是如何处理的呢?
答:底层的重命名机制将Add函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。那么借助函数重载,一个函数就有多种命名机制。
在C++调用约定(_cdecl 调用约定)中Add函数在底层被解析为:
"int __cdecl Add(int,int)" (?Add@@YAHHH@Z)"double __cdecl Add(double,double)" (?Add@@YANNN@Z)
C++中能否将一个函数按照C语言风格来编译?
答:C++可以在函数声明前加上【extern ”C“ 】使该函数按照C语言风格来编译。
6. 引用
6.1 引用的概念
引用不是新定义一个变量,而是给已存在的变量去了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
类型& 引用名(对象名)= 引用实体;
例:
void Test { int a = 10; int& ra = a; //定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); }
【注意】:引用类型必须和引用实体是同一种类型
6. 2 引用特性
引用在定义时必须初始化
一个变量可以有多个引用
引用一旦引用了一个实体,再不能引用其他的实体
6.3 常引用
//常引用 #include <iostream> using namespace std; class Test { public: void TestFunc() { const int a = 10; //int& ra = a;//编译出错a为常量 const int& ra = a;//引用类型要匹配 //int& b = 10; //编译出错b为常量 } }; int main() { Test t; t.TestFunc(); return 0; }
6.4 使用场景
做参数
做返回值
下面代码的输出结果是什么?为什么?
//引用返回的局部变量 #include <iostream> using namespace std; class Test { public: int& Add(int left, int right) { int c = left + right; return c; } }; int main() { Test t; cout << t.Add(2, 3) << endl; return 0; }
【注意】:如果一个函数返回离开时,离开函数作用域后,其栈上空间已经归还给系统,因此不能用栈上的空间作为引用类型返回。如果以引用类型返回的话,返回值的周期必须不受函数的限制(即比函数的生命周期长)。
6.5 传值和传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是十分低下的,尤其是当参数或者返回值类型非常大的时候,效率就更加的低下了。
指针和引用在作为传参或者返回值类型的时候,效率几乎一样
6.6 引用和指针的区别
在语法概念上,引用是一个别名,没有独立的空间,和其引用实体共用同一块空间
在底层实现上,引用实际上是有空间的,因为引用是用指针来实现的
【引用和指针的汇编代码对比】
【引用和指针的对比】:
引用定义必须初始化,指针没有要求
引用在引用了一个实体之后就不能再引用其他的实体,而指针可以在任何时候指向任何一个同类型的实体
没有NULL引用,但有NULL指针
在sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节数的大小(32位机下是4个字节)
引用自加1,是给引用的实体加1,指针自加1是向后偏移一个类型的大小
有多级指针,没有多级引用
访问实体方式不同,指针需要显式的解引用,引用编译器自己处理
引用比指针使用起来更安全
7. 基于范围的for循环(C++11)
7.1 范围for循环的语法
在C++11当中要循环遍历一个数组的话,不需要说明该数组的范围只需要使用基于范围的for循环。
for循环后面的括号里面内容由冒号分为两个部分,第一部分是用于迭代的变量,第二部分则表示被迭代的范围。
例:
#include <iostream> int main() { int array[10]; int i = 1; for (auto& e : array) { e = i++; } for (auto e : array) { std::cout << e << '\n'; } return 0; }
对于for范围循环,里面的原理是使用了迭代器,begin,end。
【注意】:
for范围循环与普通循环也是类似的,可以使用continue来结束本次循环,也可以用break来跳出整个循环
7.2 范围for的使用条件
for循环迭代的范围一定是固定的
对于数组来说,该迭代范围就是第一个元素与最后一个元素之间的范围。对于类来说,就是应该提供迭代器的方法begin和end。begin和end就是循环迭代的范围。
迭代的对象要实现++和==的操作
8. auto的使用
在早期的C/C++中auto是用来修饰变量的,加在某个变量的前面表示该变量是自动的变量。(其实也就是局部变量)
自动变量:
在执行流进入该函数时,自动为该变量分匹配储存空间,然后在该执行流出该函数的时候,有自动的释放该变量的存储空间。
但是在C++11对于该关键字进行了修改,auto不再是一个存储类型指示符,而变成了一个新的类型提示符,auto声明的变量必须由编译器在编译时期推导该变量的类型。
【注意】:
使用auto定义的变量必须对其进行初始化,否则不知道该变量的类型,导致编译期间错误。在编译期间编译器需要根据初始化的表达式来初始化auto的实际类型。
对于auto来说,其并非是“类型的声明”,更多的是对于类型声明的时候的“占位”,编译器会在编译期间,将auto替换为变量实际的类型。
8.1 auto与指针和引用
对于auto与指针连用可以不加*,也可以加 *。
但是auto与引用连用必须要加&。
8.2 auto的禁用
不可用auto作为函数的形参,来接收函数的实参
因为用auto作为形参的话,对于实参对于数组产过来的是一个数组,这与要求不符合,就会造成错误。
对于使用auto在同一行定义多个变量的时候,必须变量类型都一样
因为同时定义多个的话,只是使用第一个变量的类型来替换auto,如果后面的变量类型和第一个变量的类型不一致的话就会造成错误。
auto不能直接用来声明数组
对于autoC++11只保留了auto作为类型指示符的用法。
实例化模板时不能使用auto作为模板参数。
9. nullptr与nullptr_t
nullptr代表的是一个空的指针常量,nullptr的类型是nullptr_t
typedef decltype(nullptr) nullptr_t;
【注意】:
在使用nullptr表示指针空值的时候,不需要包含头文件,因为nullptr对于C++11来说是按照关键字引入的
在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同
为了提高代码的健壮性,在后续的代码中为了表示指针空值的时候最好使用nullptr