C++入门
序言:适合C语言有基础,从C语言过度进入C++的学习
文章目录
1.命名空间
在C/C++中,变量,函数和类都是大量存在的,这些变量,函数和类的名称都将存在全局作用域中,会导致冲突
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是解决这种问题的
#include<stdio.h>
int scanf = 0;//全局变量scanf
//这里将整形变量的名称定义成scanf,跟库函数中的scanf的命名冲突
int main()
{
printf("%d",scanf)
return 0;
}
//编译器报错:"scanf":重定义;以前定义是"函数"
//C语言无法解决这样的问题,C++引入命名空间
1.1定义
定义命名空间,需要使用namespace关键字,后面跟命名空间的名字,再接**{命名空间成员}**
//1.正常使用(命名空间只能全局定义)
namespace ddy//名字随意起,笔者的名字是东东羊
{
//命名空间可以定义变量/函数/类型
int scanf = 0;//变量
int Add(int x, int y)//函数
{
return x+y;
}
struct Node//类型(C语言中是结构体,C++引入了类的概念)
{
struct Node* next;
int val;
};
}
//2.命名空间可以嵌套使用
namespace N1
{
int a=0;
namespace N2
{
int a=1;//这里的a是N2的a,与N1中的a不冲突
}
}
//3.同一个工程中运行存在多个相同名称的命名空间,编译器最终会合成同一个命名空间
//test.h
namespace M1
{
int a;
int b;
int Add(int x, int y)
{
return x+y;
}
}
//test.cpp
namespace M1
{
int Sub(int x, int y)
{
return x-y;
}
}
namespace M2
{
int c;
int d;
}
//即编译器编译时,会把头文件的M1与源文件的M1合并在一起,形成的新的M1包含a,b,c,d,Add,Sub
注意: 一个命名空间就定义了一个作用域,命名空间的所有内容都局限于该命名空间中
1.2使用
namespace ddy
{
int a=10;
int b=5;
}
命名空间的使用有三种方式:
- 加命名空间名称及作用域限定符(:😃
int main()
{
printf("%d\n",ddy::a);
return 0;
}
- 使用using将命名空间中的某位成员引入
using ddy::b;
int main()
{
printf("%d\n",ddy::a);
printf("%d\n",b);//此处的b不需要再加作用域限定符
return 0;
}
- 使用 using namespace 命名空间名称 引入
using namespace ddy;
int main()
{
printf("%d\n",a);
printf("%d\n",b);//此处的a,b不再需要加作用域限定符
return 0;
}
2.C++的输入与输出
#include<iostream>
using namespace std;
//std是C++标准库的命名空间名,C++将标准库的定义实现都放到了这个命名空间中
int main()
{
cout<<"hello world!"<<endl;
return 0;
}
说明:
-
使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含头文件以及按命名空间使用方法使用std
-
cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,它们都包含在头文件中
-
cout和cin可以自动识别变量类型,不需要像C语言中的printf/scanf那样手动控制变量类型
-
***<<是流插入运算符,>>***是流提取运算符
#include<iostream>
using namespace std;
int main()
{
char a;
int b;
double c;
//cin自动识别类型
cin>>a;
cin>>b>>c;
//cout自动识别类型
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
3.缺省参数
3.1概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值
在调用函数时,没有指定的实参则用该函数的缺省值代替,有则使用指定的实参
void Func(int a = 10)
{
cout<<a<<endl;
}
int main()
{
Func(); //没有传参,就用参数的默认值
Func(1); //传参时,使用指定的实参
return 0;
}
3.2分类
- 全缺省参数
void Func(int a=1, int b=2, int c=3) //每个参数都有缺省值
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
- 半缺省参数
void Func(int a, int b=2, int c=3) //缺省参数必须从右往左给,中间不能隔着给
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
Func(1); //传参,此时a=1,b=2,c=3
Func(1,4); //传参,此时a=1,b=4,c=3
return 0;
}
说明:
- 缺省参数不能在函数声明和定义中重复出现
//test.h
void Func(int a = 10) //声明
//test.cpp
void Func(int a = 20) //定义
{}
//如果声明和定义同时出现,并且两个位置提供的缺省值不同,则编译器就无法确定该用哪一个缺省值
//解决
//test.h
void Func(int a = 10) //推荐将缺省参数写到声明中,便于在头文件里查找修改
//test.cpp
void Func(int a) //定义不写缺省参数
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
4.函数重载
4.1概念
函数重载:是函数的一种特殊情况,C++运行在同一作用域中声明几个功能相似的同名函数,这些同名的函数的形参(类型/个数/顺序)不同,常用来处理实现功能类似数据类型不同的问题
#include<iostream>
using namespace std;
//1.参数不同
int Add(int x, int y)
{
cout<<"int Add(int x, int y)"<<endl;
return x+y;
}
double Add(double x, double y)
{
cout<<"double Add(double x, double y)"<<endl;
return x+y;
}
//2.个数不同
void f()
{
cout<<"f()"<<endl;
}
void f(int a)
{
cout<<"f(int a)"<<endl; //这里输出的就是f(int a),跟a的值没有关系
}
//3.顺序不同
void f(int a, char b)
{
cout<<a<<endl;
cout<<b<<endl;
}
void f(char a, int b)
{
cout<<a<<endl;
cout<<b<<endl;
}
int main()
{
Add(1,2);
Add(1.1,2.1);
f();
f(1);
f(1,'a');
f('a',1);
return 0;
}
4.2C++支持函数重载的原理–名字修饰
环境:在Linux下,采用**g++**编译
-
在C/C++中,一个程序想要运行起来,需要经过以下几个阶段:预处理、编译、汇编、链接,这四个过程合在一起,我们也统称为编译
list.h list.c test.c
预处理:头文件展开/宏替换/条件编译/去掉注释
list.i test.i //此时的list.h已被展开在两个.c文件里
编译:检查语法,生成汇编语言
list.s test.s
汇编:汇编代码转成二进制的机器码
list.o test.o
链接:将所有的目标文件链接到一起(合并段表、符号表的合并和符号表的重定位) //C语言不支持函数重载而C++支持的区别之处
a.out
-
链接是C++支持函数重载的关键,用gcc编译C语言时,函数修饰后在符号表名字不变,而g++编译C++时,函数修饰后在符号表名字发生改变
//gcc int Add(int a, int b) //<Add> 即相同命名的函数,在链接时符号表的名字都是<Add>,此时编译器无法做出选择 {} //g++ int Add(int a, int b) //<_Z2Addii> {} int Add(char a) //<_Z1Addc> {} //g++的命名规则是:_Z + 参数数量 + 函数名称 + 参数类型首字母 //在此规则下,相同命名的函数,只要参数不同,编译都可通过,所以C++支持函数重载
-
通过命名规则,发现函数重载只与参数有关,返回值不同是不构成重载的,因为编译器没有办法区分
5.引用
5.1概念
引用不是定义一个新变量,而是给已经存在的变量取新名字,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块空间
使用方法:类型& 引用变量名 = 引用实体
int main()
{
int a = 10;
int& b = a; //定义引用类型
}
注意:引用类型必须和引用实体是同种类型的
5.2特性
-
引用在定义时必须初始化
int main() { int& a; //error int a = 10; int& b = a; //right return 0; }
-
一个变量可以有多个引用
int main() { int a = 10; int& b = a; int& c = b; //a、b、c现在是同一块空间,即:原本叫a的内存空间,现在又可以叫b、c }
-
引用一旦引用了一个实体,再不能引用其他实体
int main() { int a = 10; int b = 5; int& c = a; c = b; //是c又再引用b吗?还是把b赋值给了c //这里是把b赋值给了c,c已经引用了a,就不能再引用其他实体了 }
5.3常引用
int main()
{
const int a = 10; //加const修饰,a的权限变成只能读不能写
int& b = a; //error 原因:用未被const修饰的b来引用a,此时的权限相对于放大,变成可读可写,不允许
const int& c = a; //right
int d = 5;
const int e = d;//? right 原因:d的权限是可读可写,加const修饰的e引用后,e的权限相对d缩小,允许
return 0;
}
总结:
- 权限的大小变更只存在于引用和指针
- 权限可缩小,不可放大
5.4使用场景
-
做参数
//函数调用的三种方式:传值调用、传址调用、引用调用(C++) void Swap(int a, int b) //传值调用,无法实现交换实参的目的 { int tmp = a; a = b; b = tmp; } void Swap(int& a, int& b) //引用调用,可以实现交换实参的目的 { int tmp = a; //思考:传值调用和引用调用的这两个函数是否可以构成函数重载? a = b; //答:不构成,引用类型和引用实体是同一类型,即:int& a 的类型是int, b = tmp; //那么意味着两个Swap的参数是一样的,不构成函数重载 } void Swap(int *pa, int* pb) //传址调用,可以实现交换实参的目的 { int tmp = *pa; *pa = *pb; *pb = tmp; }
-
做返回值
//正常的返回值 int Add(int a, int b) { int ret = a+b; return ret; //ret返回的是一个临时空间,此空间开辟在栈区 } int main() { int n = Add(1,2); //n在接受ret返回的值后,ret就会被销毁 return 0; //但是需要注意的是,ret原先的空间还在那个位置,只是不再属于ret,即权限系统收回 } //引用做返回值 int& Add(int a, int b) { int ret = a+b; return ret; } int main() { int& a = Add(1,2); Add(3,4); cout<<a<<endl; //7 为什么a是7呢? return 0; //答:ret开在栈区,a引用ret,Add(1,2)结束后,ret被销毁,空间也被系统收回,此时a依然在 } //引用ret已经被收回的空间,再继续执行Add(3,4),碰巧此时的空间又被赋给新的ret,新的ret等 //于7,则a也为7 //思考:引用做返回值,如果返回值的生命周期短,出了函数就被销毁了,那么引用的返回值就不安全,不妨延长返回值的生命周期 //------------------------------------------------------------------------------------------------------- int& Add(int a, int b) { static int ret = a+b; //static修饰变量后,ret这个变量将会被开辟在静态区中,ret不会再被销毁, return ret; //即:延长ret的生命周期,直到程序结束 } int main() { int& a = Add(1,2); int& b = Add(3,4); cout<<a<<endl; //3 cout<<b<<endl; //3 为什么b也是3呢? return 0; //答:ret开辟在静态区,Add运行第一次的时候开辟,第二次就不会再开辟,用的还是第一次开辟的ret } //思考:想要保证引用返回值的安全,就要将其的生命周期延长,但延长后,后续的返回值都是第一次执行时的返回值,相当于函数只能 //执行一次,从当前所学的知识来看很鸡肋,后续再回头看
总结:一个函数想要使用引用作为返回参数,就需要返回参数出了函数的作用域还存在(全局变量/静态变量),否则就不安全
5.5传值、传引用效率比较
结论:传值调用效率低
原因:以值作为参数或者返回值类型时,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其当参数或者返回值类型非常大(例如:结构体)时,效率更低
5.6引用和指针的区别
-
在语法概念上,引用就是一个别名,不开辟新的空间,与其引用实体共用同一块空间
在底层实现上,引用是有空间的,因为引用是按照指针方式实现的
-
概念上,引用定义了一个变量的别名,指针储存一个变量地址
-
引用定义时必须初始化,指针没有要求(但还是建议将指针置成空指针)
-
引用在初始化后,不能再引用其他实体,指针在任何时候都可以指向任何一个同类型实体
-
没有NULL引用,但有NULL指针
-
在sizeof中的含义不同:引用的结果是引用类型的大小,但指针始终是地址空间所占字节个数(32位平台占4字节,64位平台占8字节)
-
引用自加,所引用的实体加1,指针自加,指针向后偏移一个类型的大小
-
有多级指针,没有多级引用
-
访问实体方式不同,指针需要解引用,引用编译器自己处理
-
引用比指针使用起来相对安全
6.内联函数
6.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,不再产生函数调用建立堆栈的开销,内联函数提升程序运行的效率
inline int Add(int a, int b)
{
return a+b;
}
int main()
{
int a = Add(1,2); //此时,Add函数被inline修饰成为内联函数,不再调用函数建立栈帧,而是直接展开
return 0;
}
//编译时
int main()
{
int a = 1+2; //Add直接展开,类似于C语言中的宏函数
return 0;
}
6.2特性
-
inline是一种空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体代替函数调用
缺陷:可能会使目标文件变大
优点:少了函数调用开销,提高了程序调用效率
-
内联函数对于编译器而言只是一个建议,不同的编译器关于inline实现的机制不同
一般建议:将函数规模较小、不是递归、且频繁调用的函数采用inline修饰,否则编译器可能会忽略inline特性
-
inline不建议声明和定义分离,分离会导致链接错误,因为inline被展开后,就没有函数地址了,链接器就会找不到
但是由于现代编译器允许链接时优化,所以即使把inline写在.cpp,也有概率被inline
面试题:
-
宏的优缺点?
优点:
- 提高代码的复用性
- 提高性能
缺点:
- 不方便调试(因为预处理阶段对宏进行了替换)
- 导致代码可读性差,可维护性差,容易误用
- 没有类型安全的检查
-
C++有哪些技术可以代替宏?
//1.常量定义换成const enum #define N 10 //换成 const int N = 10; //2.短小函数(20行以内)定义换用内联函数 #dedine Add(x,y) ((x)+(y))//宏函数 //换成 inline Add(int x, int y) { return x+y; }
7.auto关键字
环境:VS2022 64位平台
### 7.1简介
作用:在C++11中,auto是用来自动推导表达式或变量的实际类型的
#include<iostream>
using namespace std;
int main()
{
int a = 0;
auto b = a;//b此时为int类型
auto c; //error 使用auto定义变量时,必须初始化
cout<<typeid(b).name()<<endl; //typeid().name()可以自动识别变量类型
return 0;
}
注意:
使用auto定义变量时,必须对其初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型
因此auto并非是一种"类型"的声明,而是一个类型声明时的"占位符",编译器在编译阶段将auto替换成变量的实际类型
7.2使用细则
-
auto与引用和指针结合起来使用
//用auto声明指针类型时,用auto和auto*没有区别 //但auto声明引用时必须加&,即auto& int main() { int a = 10; auto pa1 = &a; auto* pa2 = &a; auto& b = a; //声明必须加& cout<<typeid(pa1).name()<<endl; //int * __ptr64 cout<<typeid(pa2).name()<<endl; //int * __ptr64 cout<<typeid(b).name()<<endl; //int return 0; }
-
在同一行定义多个变量
//当在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器会报错 //因为编译器只对第一个类型进行推导,然后用推导出来的类型来定义后面的其他变量 int main() { auto a = 1, b = 2;//right a和b都是整形,用int定义 auto c = 3, d = 4.0;//error c是整形,则auto推导出int,将c连同d一起定义成int,但d是浮点型,编译器编译失败 }
7.3注意事项
-
auto不能作为函数的参数
void TestAuto(auto a) //error,auto不能作为形参的类型,因为编译器无法对a的实际类型进行推导 {}
-
auto不能直接定义数组
void TestAuto() { int a = {1,2,3}; auto b = {4,5,6};//error }
-
为了避免与C++98的auto发生混淆,C++11仅保留了auto作为类型指示符的用法
-
auto在实际中最常见的优势用法就是跟下一节讲到的C++提供的新式for循环,还有lambda表达式等进行配合使用
8.基于范围的for循环
8.1语法
C++98中要遍历数组,可以按照一下方式进行
void TestFor()
{
int a = {1,2,3,4,5};
for(int i = 0; i<sizeof(a)/sizeof(a[0]);i++)
a[i]*=2;
}
对于一个有范围的集合而言,由程序员说明循环的范围是多余的,有时候还会犯错
因此在C++11中引入了基于范围的for循环
for(declaration : expression)
declaration表示遍历声明,在遍历过程中,当前被遍历到的元素会被储存在声明的变量中
expression是要遍历的对象,可以是表达式、容器、数组、初始化列表
void TestFor()
{
int a = {1,2,3,4,5};
for(auto e:a)
e*=2; //运行发现,这样的代码是达不到让a数组中的每个数都乘2的目的的,为什么?
//答:变量e其实相当于a数组里元素的临时拷贝,即改变e也不会a数组,那么我们就可以采用引用
for(auto& e:a)
e*=2; //这样a数组中的元素都乘2了
}
注意:范围for与普通循环类似,可以用continue来结束本次循环,也可以用break跳出整个循环
8.2使用条件
-
for循环的范围必须是确定的
对于数组而言,就是数组中的第一个元素和最后一个元素的范围
void TestFor(int arr[]) //error 当数组作为函数参数时,数组名退化为指针,不能使用范围for { for(auto e:arr) cout<<e<<endl; }
-
迭代的对象要实现++和==的操作
9.指针空值nullptr(C++11)
C++98中的指针空值
在良好的C/C++编程习惯中,定义变量最好都初始化,否则可能会产生不可预料的错误
比如一个未初始化的指针,如果一个指针没有初始化,那么我们以往都是按照下面的方式进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
}
NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义成字面的0,或者被定义成无类型指针(void*)的常量
void f(int a)
{
cout<<"f(int)"<<endl;
}
void f(int* p)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0); //f(int)
f(NULL);//f(int) 思考:NULL是指针啊,为什么会被判断为整数?
//答:因为NULL被定义成0,因此调用不了f(int* p)
f((int*)NULL); //f(int*)
}
在C++98中,字面常量0既可以是一个整数数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成一个整形常量
如果要将其按照指针的方式使用,必须对其进行强转**(void*)0**
注意:
- nullptr在C++11中,是作为新的关键字引入的,不需要再引头文件
- 在C++11中,
sizeof(nullptr)
与sizeof((void*)0)
算出来的字节大小是相等的(64位平台是8字节,32位平台是4字节) - 为了提高代码的健壮性,建议后续指针置空都用nullptr