C++
C++是一种计算机高级程序设计语言,由C语言扩展升级而产生。C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。C++引入了类和对象,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
(注:C++的源文件是以.cpp为后缀的,跟C语言不一样。)
命名空间
C语言中,自己定义的全局变量名或者函数名可能会和库里面的函数一样,产生命名重定义错误。在一个项目中,多人一起完成的时候,大概率也会有命名冲突的问题。
为了解决这种问题,c++引入了命名空间,也称为域。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突,使用namespace关键字添加命名空间。
namespace后面加一个自定义的名字(不要跟库里的命名冲突),作为以下变量和函数的命名空间域。
C++使用cout打印数据,头文件是iostream,需要和<<搭配使用,<<是流插入运算符。cout的命名空间是在std中,std是C++标准库的命名空间名字。using namespace std可以展开标准库的命名空间,可以在全局域中找到。
using namespace std;
cout << "hello world";
编译器在查找变量或者函数的时候,先在当前局部域找,再在全局域找,如果没有指定命名空间,最后是不会去命名空间去找的。指定命名空间:在变量或者函数之前加自己定义的名字::,像上面的dice::time一样。
一个域中不能出现相同的变量名或者函数名——该域的全局变量不能出现两次,函数中的局部变量不能出现两次。
命名空间可以嵌套定义。
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace dice
{
namespace game
{
}
}
命名空间的三种使用方法
加命名空间名称及作用域限定符::
std是c++标准库的命名空间,cout和endl都是c++标准库里面的,使用要在前面指定命名空间。<<是流插入运算符,endl相当于"\n"。(注:c++可以自动识别类型)
#include <iostream>
int main()
{
// ::是域作用限定符
std::cout << "hello world" << std::endl;
return 0;
}
使用using namespace命名空间名称展开命名空间。
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
使用using将命名空间里的某个成员展开。
#include <iostream>
using std::cout;
using std::endl;
int main()
{
cout << "hello world" << endl;
return 0;
}
using namespace命名空间,直接将这个域全部的变量和函数都放出来到全局域中,这样就和C语言一样了,有命名冲突的可能性。项目中尽量不要使用using namespace std;把c++的标准库直接展开,一般展开常用的和指定命名空间访问。日常练习可以直接using namesapce展开。
输入和输出
cin代表标准输入,使用右移运算符 ">>" 实现输入。cout代表标准输出设备,使用左移运算符"<<"实现输出,它们的头文件是<iostream>。endl是特殊的c++符号,表示换行。
cin和cout是对象不是函数。
cin和cout不需要像C语言一样指定数据类型输入和输出,可以自动识别类型。
自动识别类型
c++输入输出更加方便,不用像c语言那样需要指定输入输出格式,c++能够自动识别变量的数据类型。(自动识别类型的本质是函数重载)
缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值,调用函数的缺省参数如果没有传参,会使用这个缺省值。 缺省参数只能从右往左依次缺省,中间不能有不是缺省参数的。
函数有缺省参数,该函数的声明和定义不能同时出现在一个源文件中,函数声明有缺省值,定义也有缺省值,会报错,因为编译器不知道选哪个缺省值作为该函数的缺省值。
如果需要声明的时候,定义函数的参数不用给缺省值,声明给缺省值即可。
缺省参数分类
全缺省参数
函数的全部参数都给缺省值。
半缺省参数
半缺省参数必须从右往左依次给缺省值,中间不能隔开!!
想要给b缺省值,则必须也给c缺省值。
函数重载
重载函数是函数的一种特殊情况,为方便使用,C++允许在同一个域中声明或定义多个功能一样的同名函数,但是这些同名函数参数的个数 或类型 或类型顺序必须不同。
调用函数,根据实参(传参的个数 或传参类型 或类型顺序)选择不同的函数调用。重载函数常用来实现功能一样而处理的数据类型不同的函数。
不能通过函数返回类型实现函数重载。
调用函数的时候,根据传的参数自动匹配类型,最后调用对应的函数。
C++和C函数名的修饰规则
C++和C的函数名修饰规则不同,每一个源文件在汇编执行完后,会产生一个符号表,链接的时候,符号表的合并和重定位。符号表中如果有相同的函数名会报错,C++能够支持函数重载,而C语言不支持函数重载,是因为符号表中的函数名修饰规则不一样。(不同环境函数名修饰规则可能不同)。
VS2019中不能查看到符号表,在linux环境下查看符号表,具体流程不讲。
C++函数名修饰规则
linux环境下,C++的函数名修饰规则:在函数名前面加了_Z3,_Z是前缀,3是函数名的长度,后面加上了函数参数的所有类型。
int Add(int a, int b);
C函数名修饰规则
C的函数命名规则:只有函数名。
int Add(int a, int b);
引用
引用是给已经有的变量或者常量起一个别名,语法上引用不会额外开辟空间,它们指向的是同一块空间。对别名进行修改,也会修改原变量。
变量和引用的地址都是同一个地址,公用同一块空间,标识同一个变量。
引用特性
定义引用必须初始化。
一个变量可以有多个引用。
引用定义后,不能换成另一个变量的引用。
引用做参数
引用做参数,功能跟指针差不多,但是不需要解引用。大对象传参的时候,使用引用能提高效率。
引用做返回值
调用函数,引用做返回值,返回返回对象的别名,返回变量如果出了该函数的作用域销毁了,再访问该对象,会越界,对这些对象需使用传值返回;只有出了该函数的作用域依旧存在的对象,传引用返回才是正确使用。减少大对象拷贝的时间,提高效率。
变量接收引用返回对象,将引用返回对象的值赋值给变量,访问变量不会访问到引用返回对象。
引用接收引用返回对象,这个引用就是返回对象的别名,访问这个引用,就是访问返回对象。第一次访问返回对象还是10,是因为操作系统还没回收这块空间。第二次访问返回对象是随机值,操作系统已经回收了这块空间,并且将这一块的空间都置为随机值。越界访问,是错误的行为,编译器对越界访问是设岗抽查。
传引用返回,可以直接修改返回对象的值。
引用的使用
权限可以平移和缩小,但是不能放大, 这是针对指针和引用的。传值传参,形参是实参的拷贝,对形参修改不会改变实参。
// 权限平移
int a = 1;// a可读可修改
int& b = a;// b可读可修改
// 权限放大,编译出错
const int a = 1;// a可读不能修改
int& b = a;// b可读可写,编译出错
// 权限缩小
int a = 1;//a可读可修改
const int& b = a;//b可读不能修改
// 权限放大,编译出错
int& a = 10//10是常量,常量具有常属性,10可读不能修改,要用const修饰
//权限平移
const int& a = 10;//10可读不能修改,a可读不能修改
// 将int类型的值给double类型的值,会产生隐式类型转换,先将a的值放入临时变量中
// 这个临时变量是double类型的,这种临时变量不能修改,相当于被const修饰,再把临时变量赋值给b
int a = 1;
double b = a;
// 权限放大,编译出错
double& c = a;// a产生的临时变量不能修改,c可读可修改
// 正确方式
const double& d = a; // a产生的临时变量可读不能修改,d可读不能修改
函数使用引用做参数,在不改变参数的情况下,尽量使用const修饰。
引用的本质
引用的底层本质上是指针实现的,有开辟空间。观察反汇编,引用和指针,两者是一样的。
引用和指针的区别
引用的定义必须要初始化,而指针不需要。
引用语法上不需要开辟空间,指针需要4或者8个字节。
引用指向的对象不能更改为其它对象,指针可以。
引用没有空引用,指针有空指针。
引用加1是数据加1,指针加1是指针往后偏移指向类型的大小。
引用使用sizeof计算的是引用类型的大小,指针使用sizeof计算,大小是4或者8个字节。
引用使用上更加方便,指针需要解引用。
C++调用C实现的库
C语言代码。
// Hello.h
#pragma once
#include <stdio.h>
void Hello();
// Hello.c
#include "Hello.h"
void Hello()
{
printf("Hello World\n");
}
C语言代码生成库,在项目属性常规中,将配置类型修改为静态库(.lib),运行程序,就会生成一个静态库。
静态库(.lib)生成的位置。
C++程序添加C生成库。
在项目属性中,链接器常规的附加库目录添加上C生成库的路径;再在输入的附加依赖项加上库名。
C++调用C实现的库。以C的函数名修饰规则去找函数。
// test.cpp 在c++操作
// 告诉c++编译器,头文件里的函数使用的是C的库实现的,要用C的函数名修饰规则去找
extern "C"
{
// 建议直接把头文件添加到编译程序中,再包含头文件
#include "Hello.h"
}
int main()
{
Hello();
return 0;
}
C调用C++实现的库
C++代码。修改.h文件,将.c改为.cpp,以C的函数名修饰规则修饰函数,重新生成静态库,这个静态库就是C++实现的静态库。(注:此时的C++代码不能出现函数重载)
// Hello.h
#pragma once
#include <stdio.h>
// __cplusplus是c++的宏,c没有。
// 让c++以c语言的方式编译
// 通过条件编译可以让c展开c++实现的头文件的时候去掉extern "C"{}
#ifdef __cplusplus
extern "C"
{
#endif
// 函数声明
void Hello();
#ifdef __cplusplus
}
#endif
// Hello.cpp
#include "Hello.h"
void Hello()
{
printf("Hello World\n");
}
C调用C++实现的库。
// test.c 在C的操作
// 包头文件
#include "Hello.h"
int main()
{
Hello();
return 0;
}
内联函数
inline修饰的函数叫做内联函数,编译阶段c++编译器会在调用内联函数的地方展开。在c++中基本不在函数体内使用define定义的宏和常量,尽量使用const、enum、inline替代宏。
inline int Add(int a, int b)
{
return a + b;
}
内联函数的特性
内联函数是一种以空间换时间的做法,不用像函数一样建立栈帧,内联函数提升程序运行的效率。内联函数保留了宏函数的优点的同时,解决了宏函数的缺点。宏不能调试,而内联函数可以调试;宏没有类型的检查,可读性差,内联函数有类型的检查。
inline是起建议的作用,取决于编译器,如果该函数的编译指令较多,编译器也不会让inline起作用,内联函数适用于短小的函数(汇编指令少)。inline修饰一个有一百行代码的函数,调用一千次,inline如果起作用,一百乘一千就是十万行代码,程序暴增;inline不起作用,是一千一百行代码。
递归不能用inline,编译会忽略。
内联函数声明和定义不要分离,在头文件只写内联函数的声明,内联函数不会进符号表,如果内联函数所在的源文件跟使用内联函数的源文件不是同一个,会链接失败。内联函数的声明和定义不分离,都写在头文件中。
关键字auto
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。
auto使用场景
auto能够自动识别类型
auto a = 'a';// auto是char类型
auto b = 1;// auto是int类型
auto c = "abcd";// auto是char*类型
auto* d = &a;// *强调需要指针类型初始化
auto& e = b;// &是变量的别名
auto能够遍历数组(范围for)
将a数组的数据自动依次拷贝赋值给b,b是一个变量,自动迭代,自动结束。如果知道auto对应的类型也只可以直接写,这里的auto可以写成int。
auto通过引用修改数组元素
auto不能使用场景
auto不能作为函数的参数
auto不能声明数组
auto不能通过形参遍历数组
auto不能同时定义多个变量
空指针nullptr
在C++中,NULL被定义成了一个宏,#define NULL 0 。NULL变成常量,导致NULL在C++使用在有些情况下会出问题,因此C11引入了空指针nullptr,所以在C++程序中要表示空指针,以后用nullptr。