跟着Cherno学习C++(二)——基础语法1
前言
平台:Visual Studio 2022
Cherno主页地址:youtube.com/@TheCherno
B站视频搬运up主:@神经元猫
个人微信:SWPUZHH
欢迎进群交流,一起学习~~
Hello~ every guys,从现在开始,让我们跟着Cherno的教程一步一步的开始学习C++
本期我们来看一下C++的基本语法,包含变量,函数,头文件,条件与分支,循环语句,控制语句,原始指针,引用
而下一期我们开始进入面向对象的C++
变量
在讨论变量的开始请允许我列出C++中的基本数据类型(即使我很讨厌这种老套的叙述方式)
数据类型 | 占用字节(Byte) | 备注 |
---|---|---|
int | 4 | 整型 |
short | 2 | 短整型 |
long | 4 | 长整型 |
long long | 8 | 更长的整型 |
char | 1 | 单个字符 |
float | 4 | 单精度浮点数 |
double | 8 | 双精度浮点数 |
bool | 1 | 布尔类型 |
以上教条般地列出了在X64架构机器中的数据类型占用字节情况,不同的设备不同的编译器不同的平台这些占用字节情况可能略有不同,需要注意的是:
-
如果我们在这些数字类型的关键词前添加
unsigned
,则表示无符号数字,没有则表示的有正负之分,以int为例,int占用4个字节,32个bit,即2的32次方,能表示的最大数为2^32^-1=4294967295
,因为要使用一个bit来表示符号,所以表示范围应该是-2^31^~2^31^-1
,可能会有小伙伴疑惑了,有符号位的话是否存在两个0,一个+0一个-0呢?答案是不会的,在计算机中使用补码表示法,这种表示方式只会有一个0,感兴趣的小伙伴推荐问问AI”补码表示法“ -
在不同的设备平台中,这些数据类型的占用字节情况可能有所不同,所以在早期一些大的框架都会设计一套跨平台的数据结构,来确保在不同的平台,同一种数据类型占用内存大小情况相同。
-
bool类型只有True和False两种情况,按理说只要一个bit就可以了,为什么还要1个Byte来存储呢,这是因为现代计算机通常以字节为单位处理内存,即操作系统在寻址bool变量的内存位置时,只能寻址字节。在C++中0为False,非0为True。
-
可以通过sizeof来获取数据类型的实际大小
-
每种基本数据类型都有特定的用途,但是有时候也并不是用于原本的用途,基本数据类型也构成了其他自定义的数据类型
上面说了那么多“废话”,那么什么是变量呢?变量是存储数据值的容器,每个变量都有一个特点的类型,但是数据类型其实并不重要,数据类型之间唯一的区别就是,在创建变量/寻址的时候告诉编译器这个变量占用多少个字节的内存。
我想强调的是,数据类型并不重要,数据类型只是表明一个变量占用多少字节的内存,那为什么还有很多不同的数据类型呢,这其实是为了程序员编写代码方便而设定的。
函数
函数是执行特定任务的代码块,它可以被重复调用。它的一般样子是这样子:
返回类型 函数名(参数类型 变量A, 参数类型 变量B){代码块}
调用函数其实是一个进栈的过程,大量的函数调用会有频繁的进栈出栈的操作,这会影响性能,所以对于频繁调用的函数我们常常会使用inline关键字来修饰。
- inline:一个函数被声明为
inline
时,编译器会尝试将函数的代码直接插入到每个函数调用的地方,而不是进行常规的函数调用。这样做的目的是为了减少函数调用的额外开销,比如参数传递、栈帧的创建和销毁等。后续我们会进一步讨论inline关键字
头文件
先来一份比较教条的解释:在C++中,头文件(通常以 .h
或 .hpp
为文件扩展名)是一种特殊的文件,它包含了函数声明、模板定义、宏定义、类型定义、全局变量声明等,用于在多个源文件之间共享代码。头文件的主要目的是实现代码的模块化,使得程序员可以在不同的源文件中重复使用相同的代码,而无需重复编写。
#include 头文件,这句代码的意思是:在预处理阶段,将头文件的内容copy到此处
#pragma once
和
#ifndef _LOG_H
#define _LOG_H
#endif
'''以上两种做法都是来保护头文件,防止头文件被多次包含。它是头文件保护的一种方式,确保头文件中的内容在编译过程中只被包含一次,从而避免可能的重复定义错误。\
现在基本都使用第一种方式,第二种方式一般出现在遗留代码中\
'''
条件与分支
条件与分支的一般样子长这样子
if(条件语句){
分支1
}else{
分支2
}
# 或者是:
if(条件语句)
{
分支1
}
elif(条件语句)
{
分支2
}
......
else
{
分支n
}
评估条件语句,然后基于评估结果(True or False)跳转相应的分支代码,从CPU角度来看,根据评估结果,跳转到不同的内存的不同位置。所以条件和分支一般存在较大的性能消耗,后续会有一些优化方式来避免使用条件分支语句(比如switch case,数字计算设计等)。
注意:
elif()
等于else if
它是一个语法糖,底层还是else{ if(){}}
- 0为False,非0为1,NULL也是0, NULL的定义为
#define NULL 0
循环语句for while
for while的一般样子为:
for(初始化;条件检查;迭代步骤){
循环体
}
while(条件检查)
{
循环体
}
do{
循环体
}while(条件检查)
# 第三种目前使用的比较少 风格问题
控制语句continue break return
continue break 用在循环体中,continue表示跳过循环体中后续指令,进入下一次循环; break表示跳出循环体
return可以用在任何地方 表示退出这个函数
原始指针
终于来到本期的重头戏,接下来让我们愉快的讨论一下原始指针(共享指针shared_ptr,智能指针unique_ptr等指针会在后续讨论)
什么是指针呢?指针是存储内存地址的整数(让我们忘掉数据类型,数据类型毫无意义),指针的作用是帮助我们管理操纵内存
指针是存储内存地址的整数,让我们举个例子来类比指针
假如你是一名快递员,负责在一块区域的快递送上门,我们该如何知道每一个快递应该投递在那个门口呢?此时我们可以通过快递单(上面写有收货地址),来找到每个快递对应的门口。
类别一下,这个收货地址便是指针存储的内容,它是一个整数,我们通过指针来找到相应的内存地址,随后我们便可以操纵这块内存。
换句话说,指针是一个整数,它表示内存的地址,不同的内存区域,它们的地址不同
我们可以编写完全不使用指针的C++程序,但指针是一项十分强大的工具。它们提供了一种直观的方式来操作内存,从而使得数据管理和性能优化变得更加高效和灵活。
前面说到指针是一个整数,那么一个指针占用多少字节呢?这取决于操作系统,在32位系统上通常是4个字节,在64位操作系统上通常是8个字节
从上面这个例子也可以看出来,指针的大小只与操作系统的最大内存有关,而与前面的数据类型无关,此时的数据类型表示,我们来寻址的时候,寻址的大小,double*表示我们找到一块内存,这块内存的起点是指针记录的地址,大小是double的大小,即8个字节,下面让我们利用Debug来验证这一切。
打上Debug,调出内存界面,此时我们是double*,所以找到了这块大小为8个字节的内存块,它存储着一些值(此处a=0)
下面让我们换位int*再来看看情况,可以看出此出我们找到了大小为4个字节的内存块,所以不同数据类型只是代表不同的内存大小,其他没有任何区别
当我们定义一个指针=0时,表示我们定义了一个空指针,其他类似的表示还有NULL,nullptr,其中NULL的定义为 #define NULL 0
*ptr表示解引用,逆向一个指针,让我们获取到了地址中存储的值
指针作为一个整数,也存储在内存,所有指针也有指针,即存在指针的指针,三种指针等等
最后让我们再次强调一下,数据类型对内存的读写有用,它告诉编译器,内存块的大小为多少字节。
数据在内存中以小端模式存储,感兴趣的小伙伴可以搜一搜小端模式~
引用
引用实际上是指针的扩展,它是指针的伪装,语法糖。
引用本身并没有占用空间,可以理解为变量的快捷方式
&放在左侧是声明的一部分,&放在右侧表示取址
#include <iostream>
int main()
{
int a = 5;
int& ref = a;
std::cout<<ref<<std::endl;
std::cin.get();
}
'''
此处的int& 中的&是引用声明的一部分\
引用不能单独声明,在声明的时候必须赋值\
引用被初始化后就不能再指向其他变量了\
比如下部分代码,输出结果为6,ref=b,代表a=b,并没有改变绑定\
'''
#include <iostream>
int main()
{
int a = 5;
int b = 6;
int& ref = a;
ref = b;
std::cout << a << std::endl;
std::cin.get();
}
必须强调的一点,引用本身并不占用空间 上图中ref表示的就是a,在编译后的机器码中就是用a将ref替换了
让我们想象一个场景,我们将一个变量传入一个函数中,此时一般是值传递,我们只是将这个变量的值传递给了形式参数,我们在函数中修改形式参数并不会应该这个外部变量的值,如果我们想在函数调用中修改外部变量的值该怎么做呢?聪明的你一定想到了指针,我们在传递参数的时候不传递值,而是传递值对应的指针,这样我们就可以影响外部变量了。这样子当然没错,为了简化代码,我们此时常常使用引用,函数的参数类型设置为引用类型,传递引用也有同样的效果。这是因为,引用是指针的语法糖。
最后再强调一下:引用没有占用空间,它是指针的语法糖,指针更加强大。
一般是值传递,我们只是将这个变量的值传递给了形式参数,我们在函数中修改形式参数并不会改变这个外部变量的值,如果我们想在函数调用中修改外部变量的值该怎么做呢?聪明的你一定想到了指针,我们在传递参数的时候不传递值,而是传递值对应的指针,这样我们就可以影响外部变量了。这样子当然没错,为了简化代码,我们此时常常使用引用,函数的参数类型设置为引用类型,传递引用也有同样的效果。这是因为,引用是指针的语法糖。
最后再强调一下:引用没有占用空间,它是指针的语法糖,指针更加强大。