一..h
头文件书写
如果你长时间只是写一个
.c
或.cpp
文件作为一个项目,那么很少会写.h
头文件,但是它是我们必须要会的,曾在某公司的笔试题目中考过,所以第一部分就介绍.h
头文件的书写模板。
在写头文件时需要注意,在开头和结尾处必须按照如下样式加上预编译语句(如下):
#ifndef 你的文件名
#define 你的文件名 //两个文件名保持一致
// 你的代码写在这里
#endif
这样做是为了防止重复编译,不这样做就有可能出错。
我们在理解的基础上记忆,比如ifndef
其实是if not define
,如果没有定义该文件,那么就define xxx.h
定义该文件,具体的定义在下面实现,而endif
用来表示结束。曾经笔试的一道题目就是.h
头文件的格式和作用,那么上面就是答案啦!
二.C++引用
今天的主要部分就是C++引用了,顺带一些其他很少讲解但实用的知识点。
1.基本用法
作用:给变量起一个别名
语法:数据类型 &别名 = 原名
int a=10;
int &b=a;
从内存的角度来理解这段简单代码,int a=10
表示在内存中分配了一个4B
的空间,其存放的值是10,我们可以通过变量a
来对这块内存空间进行操作,同时第二句代码给a起一个别名叫b,那么以后我们也可以通过b来操作这块内存空间。那么如果你通过b来改变了这块内存的空间的值,那么a代表的内存空间的值也发生变换。
引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。
2.引用注意事项
- 引用创建时就必须初始化(也就是说一定要通过=告诉计算机这个引用是哪个变量的别名)
- 引用一旦初始化后就不能更改
3.引用作为函数参数
在定义或声明函数时,我们可以将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有“在函数内部影响函数外部数据”的效果。
对于函数的参数传递,一般有三种传递方法:值传递、指针传递、引用传递。对于值传递我们知道,形参并不改变实参,最简单等的例如交换函数swap
:
void swap(int a,int b){
int temp=a;
a=b;
b=temp;
//可以在这里使用cout输出a和b
}
int main(){
int a=10,b=20;
swap(a,b);
//也可以在这里使用cout输出a和b
}
也就是说,如果使用的值传递,在main
函数里的cout
输出你会发现并没有交换a和b两个变量的值,只有在swap
函数里的cout
才会交换两个变量的值,针对这个问题我们采取了引用传递的办法,这些形参就会带动实参的改变。(如果你对指针传递很熟悉,也可以使用指针传递)
4.引用作函数返回值
引用作为函数的返回值时,需要注意以下两点:
- 不要返回局部变量的引用
- 如果返回值是引用类型,那么函数调用可以作为左值
第一点好理解,局部变量即在函数内部定义的变量,我们不能返回它的引用;主要是第二点怎么理解,直接看代码:
#include <iostream>
//声明命名空间std
using namespace std;
int &func() {
static int a = 10;
return a;
}
int main() {
int &result = func();
cout << "&result=" << result << endl;
func() = 20; //函数调用可以整体作为左值进行赋值,其实是对a的内存空间存储的数进行修改
cout << "&result=" << result << endl;
return 0;
}
一般情况下函数调用都是作为右值来进行使用的。
5.引用的本质
请确保你已了解相关知识点,或者看过我昨天的博客:从C向C++1
引用的本质其实是一个指针常量,我们知道指针常量它是永远指向一个地址的常量,但是对应地址的内存空间存储的值是可以改变的,这也是为什么引用初始化后不能再指向其他变量但可以通过引用修改其内存空间的值。
int &ref=a;
//int *const ref = a 这两行代码是等价的
在使用引用的过程中,计算机其实就是把它理解为一个指针常量,只不过调用引用时计算机已经在背后完成的指针的相关操作,使其看起来就是一个引用。
三.引用补充
1.常量引用
作用:常量引用用来修饰形参,防止误操作
int a=10;
int &ref=a; //正确使用引用
int &ref=10; //错误代码,因为引用的本质是一个指针常量,必须指向的是一个变量也就是非常量
const int & ref = 10; //正确的,等价于int temp=10;int &ref=temp;
一般来说,引用是不支持指向常量的, 或者说必须与const
关键字搭配,他才能指向构成一个常量引用(相当于一个缺少了原名只有别名的引用)。但是因为加入了const
关键字,虽然我们可以通过别名来访问这片内存空间,但是const
导致了其值不能再发生变换,也就是说其只能永远是10。
在实际的开发过程中,我们经常使用的是引用传递函数参数,这样如果在函数内部对形参进行了误操作那么实参也会发生改变,所以经常在形参前面加一个const
关键字,也就是为了防止函数内部对其实参做了某种修改。
2.引用与指针
- 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,以后也能指向任意数据。
- 指针可以有多级,但是引用只能有一级,例如,
int **p
是合法的,而int &&r
是不合法的。如果希望定义一个引用变量来指代另外一个引用变量,那么也只需要加一个&
,如下所示:
int a = 10;
int &r = a;
int &rr = r;
- 指针和引用的自增(++)自减(--)运算意义不一样。对指针使用 ++ 表示指向下一份数据,对引用使用 ++ 表示它所指代的数据本身加 1;自减(--)也是类似的道理。
重点:在计算机408考研中的计组会涉及。
3.临时数据
其实 C++ 代码中的大部分内容都是放在内存中的,例如定义的变量、创建的对象、字符串常量、函数形参、函数体本身、new
或malloc()
分配的内存等,这些内容都可以用&
来获取地址,进而用指针指向它们。除此之外,还有一些我们平时不太留意的临时数据,例如表达式的结果、函数的返回值等,它们可能会放在内存中,也可能会放在寄存器中。一旦它们被放到了寄存器中,就没法用&
获取它们的地址了,也就没法用指针指向它们了。
寄存器离 CPU 近,并且速度比内存快,将临时数据放到寄存器是为了加快程序运行。但是寄存器的数量是非常有限的,容纳不下较大的数据,所以只能将较小的临时数据放在寄存器中。int、double、bool、char
等基本类型的数据往往不超过 8 个字节,用一两个寄存器就能存储,所以这些类型的临时数据通常会放到寄存器中;而对象、结构体变量是自定义类型的数据,大小不可预测,所以这些类型的临时数据通常会放到内存中。
诸如 100、200+34、34.5*23、3+7/3 等不包含变量的表达式称为常量表达式。常量表达式由于不包含变量,没有不稳定因素,所以在编译阶段就能求值。编译器不会分配单独的内存来存储常量表达式的值,而是将常量表达式的值和代码合并到一起,放到虚拟地址空间中的代码区(下一部分写程序的分区)。总起来说,常量表达式的值虽然在内存中,但是没有办法寻址,所以也不能使用&
来获取它的地址,更不能用指针指向它。
引用和指针在本质上是一样的,引用仅仅是对指针进行了简单的封装。引用和指针都不能绑定到无法寻址的临时数据,并且 C++ 对引用的要求更加严格,在某些编译器下甚至连放在内存中的临时数据都不能指代,这也是单纯的引用无法指向常量的原因。
总结起来说,给引用添加
const
限定后,不但可以将引用绑定到临时数据,还可以将引用绑定到类型相近的数据,这使得引用更加灵活和通用,它们背后的机制都是临时变量。
四.程序的内存模型
1.内存分区模型
C++程序在执行时,将内存大方向划分为4个区域:
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区: 存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值局部变最等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
2.程序运行前
在程序编译后,生成了.exe
可执行程序,未执行该程序前分为两个区域:
- 代码区:
- 存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
- 全局区:
- 全局变量和静态变量(
static
关键字)存放在此 - 全局区还包含了常量区,字符串常量和其他常量(
const
修饰的全局变量)也存放在此 - 该区域的数据在程序结束后由操作系统释放
注意:
const
修饰的局部变量叫局部常量,其并不在全局区。
3.程序运行后
- 栈区:由编译器自动分配释放,存放函数的参教值,局部变量等。
注意事项: 不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
因为如果某个函数返回的是其局部变量的地址,在其函数执行完后,该函数对应的栈内存空间被自动释放,你在main
函数里调用该函数的返回值也得不到你想要的数据。
- 堆区:由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。
在C++中主要利用new在堆区开辟内存,在C语言中用malloc
开辟内存