当前位置: 首页 > news >正文

C++ 入门

C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等

文章目录

  • 一、命名空间
  • 二、输入输出
  • 三、缺省参数
  • 四、函数重载
  • 五、引用
    • 1. 引用的用法
    • 2. 常引用
    • 3. 引用的使用场景
    • 4. 引用的底层实现
  • 六、内联函数
  • 七、auto 关键字(C++11)
  • 八、范围 for -- 语法糖(C++11)
  • 九、nullptr(C++11)

C++ 为了解决 C语言中存在的一些不好的地方,从而增加了一些语法

一、命名空间

在项目组中,进行代码合并时,可能会遇到自己和其他人写的代码,定义了相同的名字,发生命名冲突,在C语言中,只能在产生错误后,修改名字,没有很好的预防手段

在这里插入图片描述

自己定义的变量名,函数名,类型名等,都可能会和别人定义的名字冲突,或者和库的名字冲突,因为此时相同的名字处在同一个域(全局域)

而在不同的域定义了相同的名字,并不会产生命名冲突,使用时是局部优先

C++ 中增加了另一种新的域,叫做命名空间,用 namespace 关键字定义

命名空间定义:
namespace 空间名
{
	//类型,变量,函数等
	
}	//花括号后没有分号

将处在全局域中的变量,函数,类型等,封装在命名空间域中,便可以防止命名冲突
在这里插入图片描述

在命名空间域中也包含全局域和局部域,因此不会影响变量的生命周期

在命名空间域中,内部对内部的访问,也是遵守先声明后使用,优先使用局部,然后使用命名空间域中的全局,最后使用命名空间域外的全局

命名空间域会影响编译时期的查找规则在查找一个变量、函数、类型是否定义时,默认是在局部找(代码块内),局部不存在便会去全局找,全局找不到便会报错,不会直接去已经定义的命名空间中找

外部想要访问命名空间内部的内容时:有三种方法

  • 指定命名空间访问,使用域作用限定符 :: (两个冒号)
  • 展开命名空间中的所有内容,using namespace 命名空间名
  • 展开命名空间中的常用内容,using 命名空间名 :: 内容

C++ 中,将标准库函数的代码放在 iostream 头文件中,并用 std 命名空间封装,防止用户和库中的命名冲突,这里以 cout 为例

//指定命名空间访问
//:: 域作用限定符,左边为空,代表在全局域中查找
#include <iostream>

int main()
{
	std::cout << "hello world";
	
	return 0;
}

//展开命名空间中的所有内容
#include <iostream>

using namespace std;

int main()
{
	cout << "hello world";

	return 0;
}

//展开命名空间中的常用内容
#include <iostream>

using std::cout;

int main()
{
	cout << "hello world";

	return 0;
}

平时练习时可以使用全部展开,写项目时建议展开常用的和指定访问

还需要注意几点

  • 命名空间的名字相同时,会合并为一个命名空间,但是名字相同的命名空间中,如果存在相同的定义会报错
  • 命名空间可以定义在命名空间中,即命名空间可以嵌套定义

二、输入输出

C++ 中标准库提供的 cin 可以用于输入,cout 可以用于输出,cin 和 cout 自动识别类型,不用指定输入输出格式,cout 中 endl 表示换行

#include <iostream>

//为了可以找到封装在 std 中的 cin 和 cout
using namespace std;

int main()
{
    char c;
    int i;
    double d;

	//自动识别类型,cin >> 可以简单理解为数据从控制台流向变量
    cin >> c >> i >> d;
    
    //自动识别类型,输出数据后换行,cout << 可以简单理解为数据从变量流向控制台
    cout << c << endl;
    cout << i << endl;
    cout << d << endl;
    cout << "hello C++" << endl;
    
    return 0;
}

三、缺省参数

缺省参数是声明或定义函数时为函数的参数指定一个缺省值,在调用该函数时,如果省略指定实参则采用该形参的缺省值,否则使用指定的实参。

声明或定义函数时只能从右往左连续的为函数的参数指定缺省值,调用函数传参时,也只能从右往左连续省略实参

#include <iostream>

using namespace std;

//只能从右往左连续的为函数的参数指定缺省值
//void f1(int a, int b = 3, int c) -- 错误写法
void f1(int a, int b = 3, int c = 10)
{
    cout << a << " ";
    cout << b << " ";
    cout << c << endl;
}

int main()
{
    //只能从右往左连续省略实参
    //f1(1, , 3); -- 错误写法
    f1(1);
    f1(1, 2);
    f1(1, 2, 3);

    return  0;
}

注意:

  • 有缺省参数时,声明和定义不能同时给缺省参数,推荐在声明给,如果出现声明和定义分离(多文件时),只能在声明给,定义时不给
  • 缺省值只能是常量或全局变量(一般不使用全局变量)
//报错:非法将局部变量作为缺省参数
void f1(int a, int b = a)
{
	cout << a << endl;
}

四、函数重载

C++ 允许 在同一作用域中 声明几个功能类似的 同名函数,但是需要满足这些同名函数的形参列表(参数个数 或 参数类型 或 参数类型顺序)不同,常用来处理实现功能类似,但参数的数据类型不同的问题

#include <iostream>

using namespace std;

//参数个数不同
void f1()
{
	cout << "f1()" << endl;
}

void f1(int a)
{
	cout << "f1(int a)" << endl;
}

//参数类型不同
int Add(int A, int B)
{
	return A + B;
}

double Add(double A, double B)
{
	return A + B;
}

//类型顺序不同
void f2(int i, char c)
{
	cout << "f2(int i, char c)" << endl;
}

void f2(char c, int i)
{
	cout << "f2(char c, int i)" << endl;
}

int main()
{
	//参数个数不同
	f1();
	f1(1);

	//参数类型不同
	cout << Add(1, 2) << endl;
	cout << Add(1.1, 2.2) << endl;

	//参数顺序不同
	f2(1, 'a');
	f2('a', 1);

	return 0;
}

未重载的函数,调用函数传参类型不同时,会隐式转换

重载的函数,调用函数时,实参的参数列表所有重载的函数的参数列表 都不匹配时,不会隐式转换,编译器无法分辨调用哪个函数,便会报错

C++ 函数重载的原理 – 对函数名修饰
以 Linux 下的 gcc/g++函数名修饰规则为例

用 gcc 编译 .c 文件后,用 objdump -S 可执行程序 指令,查看函数名修饰规则
在这里插入图片描述
用 g++ 编译 .cpp 文件后,用 objdump -S 可执行程序 指令,查看函数名修饰规则
在这里插入图片描述
在编译时期对函数名进行修饰之后,便可以根据调用函数时给的参数类型,确定函数调用的是哪个重载函数,因此重载函数只会影响编译速度

函数调用时,返回值可以选择性的使用,因此只有返回值不同的同名函数,调用时无法区分,便不能构成重载

五、引用

1. 引用的用法

//定义引用
//类型& 引用名 = 引用实体;
int a = 10;
int& ra = a;

在使用上:引用不是定义新变量,而是给指定的变量取别名,和该变量共用同一块空间

在这里插入图片描述

#include <iostream>

using namespace std;

int main()
{
	//定义变量 i
	int i = 0;
	
	//定义新变量 j, i 和 j 占用不同的空间
	int j = i;
	
	//给变量 i 取别名, i 和 k 占用相同的空间
	int& k = i;
	
	//i 和 j 的地址不同
	//i 和 k 的地址相同
	cout << &i << " ";
	cout << &j << " ";
	cout << &k << endl;
	
	++i;
	++j;
	++k;
	
	//结果为 2 1 2
	cout << i << " ";
	cout << j << " ";
	cout << k << endl;

	return 0;
}

注意

  • 引用是变量的别名
  • 引用在定义时必须初始化
  • 变量可以有多个引用,可以给引用初始化为变量的别名(给别名取别名,都是同一个变量)
  • 引用指定实体后,不能在引用其他实体
  • 没有 NULL 引用
  • 在 sizeof 中,引用结果为引用类型大小
  • 没有多级引用,引用指针,引用数组,可以按以下内容理解
    引用不需要开辟新空间,没有引用的引用
    引用不需要开辟新空间,没有引用的指针
    引用不需要开辟新空间,不能作为数组元素(数组元素有空间),即不存在引用数组

2. 常引用

C++ 中,指针和引用在使用时,权限只能保持或缩小,但是 权限不能放大

先以指针为例:

#include <iostream>

using namespace std;

int main()
{
	//指针权限放大 - error
	//i 是常变量,不可以被修改
	//int* 可以通过解引用修改变量的值
	//不可以修改 --> 可以修改 权限放大(不允许)
	const int i = 0;
	int* pi = &i;	//报错,const int* 无法转换为 int*

	//指针权限保持
	//i 是常变量,不可以被修改
	//const 修饰 *pi,因此 pi 不能通过解引用修改变量 i 的值
	//不可以修改 --> 不可以修改 权限保持
	const int i = 0;
	const int* pi = &i;

	//指针权限缩小
	//i 是变量,可以被修改
	//const 修饰 *pi,因此 pi 不能通过解引用修改变量 i 的值
	//可以修改 --> 不可以修改 权限缩小
	int i = 0;
	const int* pi = &i;

	return 0;
}

引用也是如此:

#include <iostream>

using namespace std;

int main()
{
	//引用权限放大 - error
	const int i = 0;
	int& ri = i;	//报错,const int 无法转换为 int&

	int& r = 10;	//报错

	//引用权限保持
	const int i = 0;
	const int& ri = i;

	const int& r = 10;

	//引用权限缩小
	int i = 0;
	const int& ri = i;

	return 0;
}

3. 引用的使用场景

(1) 输出型参数:

#include <iostream>

using namespace std;

//引用做参数,可以直接修改实参的值
void Swap(int& r1, int& r2)
{
    int tmp = r1;
    r1 = r2;
    r2 = tmp;
}

int main()
{
    int a = 10, b = 20;
  
    cout << a << " " << b << endl;

	//直接传变量 a 和 b;
    Swap(a, b);

    cout << a << " " << b << endl;

    return 0;
}

输出:
10 20
20 10

(2) 返回值引用:

在函数栈帧的知识中,函数的返回值是通过临时变量返回的(为什么不能直接返回?因为函数调用结束,回到调用函数处时,被调用函数的栈帧已经还给操作系统了),如果返回值较小,则使用寄存器,如果返回值较大,则在调用函数的栈帧中开辟

下述代码中,变量 n 在静态区,当函数调用结束,回到调用函数的栈帧中时,变量 n 不会被销毁但是编译器还是会通过临时变量的方式带回返回值

int Count()
{
   static int n = 0;
   n++;
   
   // ...
   
   return n;
}

int main()
{
	int ret = Count();
	return 0;
}

此时程序员可以自行调整代码,将返回值设置为引用,便不会使用临时变量,而是使用原空间的别名

int& Count()
{
   static int n = 0;
   n++;
   
   // ...
   
   return n;
}

int main()
{
	int ret = Count();
	return 0;
}

注意:不要返回当前函数栈帧中的变量的引用

#include <iostream>

using namespace std;

int& Add(int a, int b)
{
	int c = a + b;
	
	return c;
}

int main()
{
	int& ret = Add(1, 2);

	//ret 的结果是随机值
	cout << "Add(1, 2) is :" << ret << endl;

	return 0;
}

什么情况下可以返回引用:调用函数结束,回到调用函数处时,变量不会销毁的,都可以做为引用返回
如:在堆上开辟的空间,静态区,参数中的引用等

引用做返回值时,’ 可以对函数的返回值进行修改 ',在之后学习的运算符重载中作用很大

传引用明显的比传值效率高
参数传值时,形参是实参的临时拷贝,返回值传值时,使用的是临时变量
参数传引用时,形参是实参的别名,返回值传引用时,使用的是变量的别名

4. 引用的底层实现

实现上:引用是由指针实现的,也是需要开辟空间的,虽然实现上是这样,但是为了便于使用引用,还是以使用上的概念为主

int main()
{
	int a = 0;

	//实现上,将 a 的地址赋给指针 ra
	int& ra = a;

	//实现上,编译器默认将 ra 解释为(*ra)
	//因此,&ra 解释为 &(*ra) == &a 所以 &a 和 &ra 的地址一样
	ra = 10;

	int* pa = &a;
	*pa = 10;

	return 0;
}

//启动调式,右键进入反汇编
	int a = 0;
00007FF71DA317AD  mov         dword ptr [a],0  

//引用和指针的指令,在汇编层面一模一样
	int& ra = a;
00007FF71DA317B4  lea         rax,[a]  
00007FF71DA317B8  mov         qword ptr [ra],rax  
	ra = 10;
00007FF71DA317BC  mov         rax,qword ptr [ra]  
00007FF71DA317C0  mov         dword ptr [rax],0Ah  

	int* pa = &a;
00007FF71DA317C6  lea         rax,[a]  
00007FF71DA317CA  mov         qword ptr [pa],rax  
	*pa = 10;
00007FF71DA317CE  mov         rax,qword ptr [pa]  
00007FF71DA317D2  mov         dword ptr [rax],0Ah

引用类型必须和引用实体是同种类型的,不同类型无法直接赋值,但可以通过以下两种方式

在 C/C++ 中,显示/隐式类型转换是通过临时变量来完成的,临时变量具有常属性(不可以被修改)

#include <iostream>

using namespace std;

int main()
{	
	int a = 2;
	double& ra1 = (double&)a;
	const double& ra2 = a;

	//输出 2 -9.25596e+61 2
	cout << a << " " << ra1 << " " << ra2 << endl;

	return 0;
}

//启动调式,右键进入反汇编
	int a = 2;
00007FF7780D23CD  mov         dword ptr [a],2  
//mov 将 2 放到 a 中

	double& ra1 = (double&)a;
00007FF7780D23D4  lea         rax,[a]  
00007FF7780D23D8  mov         qword ptr [ra1],rax 
//lea 取出 a 的地址放到 rax
//mov 将 rax 中的内容放到指针 ra1 中

	const double& ra2 = a;
00007FF7780D23DC  cvtsi2sd    xmm0,dword ptr [a]  
00007FF7780D23E1  movsd       mmword ptr [rbp+68h],xmm0  
00007FF7780D23E6  lea         rax,[rbp+68h]  
00007FF7780D23EA  mov         qword ptr [ra2],rax  
//cvtsi2sd 取出 a 中低 64 位,将其转换为浮点数,放到 xmm0
//movsd 将 xmm0 放到临时变量 rdb+68h(h 表示十六进制) 空间中
//lea 取出 rdb+68h 空间的地址放到 rax
//mov 将 rax 中的内容放到指针 ra2 中

引用 ra1 是 a 的别名,因此 ra1 会以浮点数的方式看待 a 中的值(这里可以修改 ra1,存在问题,待以后解决)
引用 ra2 是赋值时隐式转换过程中 临时变量的别名,因此 ra2 是以浮点数的方式看待浮点类型的临时变量的值

引用比指针使用起来更安全

六、内联函数

在 C语言中,宏的缺点:1. 不能调试 2. 没有类型检查 3. 有些场景下非常复杂,容易出错,宏的优点:1. 速度快(不用创建函数栈帧) 2. 类型不固定

在 C++ 中,为了弥补宏的缺点,推荐 用 const 和 enum 代替 宏常量,用 内联函数 代替 宏函数

用 inline 修饰的函数称作内联函数(可以调试,有类型检查,书写和函数一样容易记忆),编译时 C++ 编译器会 在调用内联函数的地方展开(速度快),

为了可以在 debug 模式观察到内联函数的展开,需要对编译器进行设置(因为 debug 模式下,编译器默认不会对代码进行优化)
在 vs 2022 中
右击项目 -> 属性 -> C/C++ 下的常规 -> 调式信息格式 改为 程序数据库 (/Zi)
右击项目 -> 属性 -> C/C++ 下的优化 -> 内联函数扩展 改为 只适用于 __inline (/Ob1)

#include <iostream>

using namespace std;

//内联函数
inline void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

int main()
{
	int a = 10, b = 20;
	Swap(a, b);

	cout << a << " " << endl;

	return 0;
}

//启动调式,右键进入反汇编
//汇编指令中,函数调用时需要通过 call 指令,跳转到函数地址
//Swap 调用时的汇编指令中没有 call,也就说明内联函数展开了
	Swap(a, b);
00007FF64AB41552  mov         eax,dword ptr [a]  
00007FF64AB41556  mov         dword ptr [rsp+60h],eax  
00007FF64AB4155A  mov         eax,dword ptr [b]  
00007FF64AB4155E  mov         dword ptr [a],eax  
00007FF64AB41562  mov         eax,dword ptr [rsp+60h]  
00007FF64AB41566  mov         dword ptr [b],eax  

内联函数特性:

  • 函数用 inline 修饰,对编译器而言只是一个建议,在编译阶段是否会展开,取决于编译器,不加 inline 修饰的函数不会展开
    建议 将函数规模小,不是递归并且频繁调用的函数 用 inline 修饰,否则编译器会忽略 inline 特性
  • 内联函数是一种以空间换时间的方法
    优点:少了函数调用开销,提升效
    缺点:可执行程序变大
  • 内联函数如果声明和定义分离,会导致链接错误
    编译时会将 F.h 中的声明拷贝到 test.cpp 中,此时 test.cpp 中只有内联函数 f 的声明,没有内联函数 f 的定义,于是便无法将 f 函数展开(.cpp 文件是单独编译的),并且由于内联函数会展开,所以内联函数 f 并不会在 test.cpp 的符号表中产生,也就导致在链接时合并符号表后无法找到函数 f

    书写内联函数时,在 .h 文件中 定义

七、auto 关键字(C++11)

在早期的 C/C++ 中 auto 用来表示变量具有自动存储器的局部变量(但是一直没有人使用 auto)

到了 C++11,标准委员会赋予了 auto 全新的含义,auto 不再是一个存储类型指示符,而是一个新的类型指示符(为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法)

auto 声明的变量,由编译器在编译时期 根据初始化的值的类型 自动推到变量的类型

#include <iostream>

using namespace std;

int main()
{
	//auto 声明的变量必须初识化
	//auto a; --error

	//auto 自动推导变量类型
	auto c = 'a';
	auto i = 10;
	auto d = 3.14;

	//auto 声明指针时 auto 和 auto* 没有区别,但 auto* 一定要初识化为地址
	auto p1 = &i;
	auto* p2 = &i;

	//引用类型必须用 auto&
	auto& r = i;

	//typeid 是操作符,typeid(变量).name() 获取变量的类型
	//输出:char int double int * __ptr64 int * __ptr64 int
	cout << typeid(c).name() << " ";
	cout << typeid(i).name() << " ";
	cout << typeid(d).name() << " ";
	cout << typeid(p1).name() << " ";
	cout << typeid(p2).name() << " ";
	cout << typeid(r).name() << endl;
	
	return 0;
}

auto 不是一种 “类型” 的声明,而是一种类型声明时的 “占位符”,编译器在编译期会将 auto 替换为变量实际的类型,所以 auto 声明多个变量时,编译器只会对第一个变量的类型进行推导,然后将 auto 替换为推导的类型,因此每个变量的类型应该相同(不相同会编译错误)

auto 不能作为函数的参数,因为编译时无法知道实参的类型

auto 不能用来声明数组

在以后的学习中,某些类型较长,容易写错,此时便可以使用 auto 关键字自动推导,这种情况下也可以使用 typedef 来解决,不过 typedef 在某些情况存在一些问题

typedef char* pstr;

int main()
{
	//编译报错,const 修饰 p1,需要指定初始化
	//const pstr p1;

	//const 修饰 p2,编译通过
	const pstr* p2;

	return 0;
}

八、范围 for – 语法糖(C++11)

C++11 中引入了基于范围的 for 循环

范围 for 循环后的括号由冒号 : 分为两部分
第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

#include <iostream>

using namespace std;

int main()
{
	int array[] = { 1, 2, 3, 4, 5, 6 };

	//自动将数组 array 中的元素依次赋值给 i,自动判断结束
	//i 是循环变量,auto 是循环变量的类型
	for (auto i : array)
	{
		cout << i << " ";
	}
	cout << endl;

	return 0;
}

注意:和普通循环一样,可以使用 continue 和 break

九、nullptr(C++11)

在定义指针变量时,如果不知道指针的指向,通常会将指针变量指向 NULL

在 C++98 中,NULL实际是一个宏,在传统的 C 头文件 stddef.h 中

//在 C++ 中,NULL 被定义成 0
//在 C 语言中,NULL 被定义成 ((void *)0)
#ifndef NULL
    #ifdef __cplusplus
        #define NULL 0
    #else
        #define NULL ((void *)0)
    #endif
#endif

由于在 C++98 中 NULL 被定义为了 0,在使用 NULL 时,难免会遇到一些麻烦

#include <iostream>
#include <stddef.h>

using namespace std;

void f(int a)
{
	cout << "f(int)" << endl;
}

void f(int* p)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);

	return 0;
}

输出:
f(int)
f(int)

程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖

C++11 中,引入了新的关键字 nullptr,表示指针空值((void*)0),sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同

相关文章:

  • 机器学习研究的 12 个宝贵经验
  • LabVIEW更高的吞吐量与更少的延迟2
  • 【C语言练习】杨氏矩阵、杨辉三角
  • 【Java】到底什么是包?|最通俗易懂讲解|保姆级
  • 【YBT2023寒假Day4 B】人人人数(数学)
  • MasterSlave概念与配置与eeprom信息擦除解决步骤
  • FPGA和CPLD芯片选型介绍(二)
  • 【C++】继承
  • 第九层(10):STL之函数对象
  • 边缘检测与角点检测(模式识别与图像处理课程作业)
  • STL使用方法(C++)
  • 懂了委托,才算真正入门C#
  • 2023 年 1 月的5篇深度学习论文推荐
  • 数据结构:简单排序方法(插入排序和起泡排序)
  • 百度、字节终于不再相互“抄袭”
  • [deviceone开发]-do_Webview的基本示例
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • 【腾讯Bugly干货分享】从0到1打造直播 App
  • Android交互
  • Angular数据绑定机制
  • co.js - 让异步代码同步化
  • iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码
  • JAVA之继承和多态
  • JS笔记四:作用域、变量(函数)提升
  • NSTimer学习笔记
  • Sass 快速入门教程
  • 阿里云购买磁盘后挂载
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 反思总结然后整装待发
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 精益 React 学习指南 (Lean React)- 1.5 React 与 DOM
  • 力扣(LeetCode)21
  • 我的面试准备过程--容器(更新中)
  • 阿里云服务器购买完整流程
  • 你学不懂C语言,是因为不懂编写C程序的7个步骤 ...
  • # .NET Framework中使用命名管道进行进程间通信
  • #if 1...#endif
  • #include
  • #Linux(make工具和makefile文件以及makefile语法)
  • #经典论文 异质山坡的物理模型 2 有效导水率
  • #我与Java虚拟机的故事#连载01:人在JVM,身不由己
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (arch)linux 转换文件编码格式
  • (AtCoder Beginner Contest 340) -- F - S = 1 -- 题解
  • (Oracle)SQL优化技巧(一):分页查询
  • (超详细)语音信号处理之特征提取
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (四) Graphivz 颜色选择
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (转)母版页和相对路径
  • (转)人的集合论——移山之道
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题