C语言/C++ 基础知识
1.面试常问关键字
Tips:去背诵概念完全没有任何的意义,需要你能结合一些例子去记忆。
1.1 volatile 关键字
- 作用:告诉编译器该变量可能会被外部因素改变,禁止优化。 即告诉编译器每次存取该变量的时候都要从内存去存取而不是使用它之前在寄存器中的备份。
什么是编译器优化?
我们首先去理解CPU 的工作原理。CPU在执行程序的时候会将指令从内存中取出来,然后放到寄存器中执行。 寄存器是CPU内部的高速缓存,速度非常快。 编译器优化就是为了提高程序的执行效率,减少不必要的内存访问和计算。
例如,如果一个变量在程序中没有被修改,编译器可以将它的值缓存到寄存器中,而不是每次都从内存中读取。 所以我们就需要来讲一下内存读取规则。
int a, b;// 为a,b申请内存
a = 1; // 1 -> CPU
// CPU -> 内存(&a)
b = a; // 内存(&a) -> CPU
// CPU -> 内存(&b)
- 如代码所示,a = 1这个程序,先将1写入CPU,再从CPU中将1写入a所在的内存地址中;
- b = a是先从内存中将a的值取出到CPU,再从CPU将值存入b的内存地址中。
int a = 1 , b, c; // 为a,b,c申请内存并初始化
b = a; // 内存(&a) -> CPU
// CPU -> 内存(&b)
c = a; // * 内存(&a) -> CPU *
// CPU -> 内存(&c)
如上图代码所⽰,上边的程序如果按第⼀段代码所说的顺序执⾏,则c = a语句在编译时 是可以被编译器优化的,即注释部分(* 内存(&a) -> CPU *)的内容不被执⾏,因为在b = a这个语句中,a已经被移⼊过寄存器(CPU),那么在执⾏c = a时,就直接将a在寄存器 (CPU)中传递给c。这样就减少了⼀次指令的执⾏,就完成了优化。
上⾯就是编译器优化的原理过程,但是这个过程,有时会出现问题,⽽这个问题也就 volatile存在的意义!
volatile的引⼊
上边程序中,如果在执⾏完b = a后,a此时的值存放在CPU中。但是a在内存中⼜发⽣ 了变化(⽐如中断改变了a的值),但是存在CPU中的a是原来未变的a,按理应该是已经变 化后的a赋值给c,但是此时却导致未变化的a赋值给了c。
这种问题,就是编译器⾃⾝优化⽽导致的。为了防⽌编译器优化变量a,引⼊了volatile 关键字,使⽤该关键字后,程序在执⾏时c = a时,就会先去a的地址读出a到CPU,再从 CPU将a的值赋予给c。这样就防⽌了被优化。
volatile int a = 1; // 只有 a 是 volatile
int b, c; // b、c 普通变量
b = a; // 每次都从内存(&a)读
c = a; // 每次都从内存(&a)读
哪些情况下使⽤volatile
(1)并⾏设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随 时可以被外设硬件修改。当声明指向设备寄存器的指针时⼀定要⽤volatile,它会告诉编译 器不要对存储在这个地址的数据进⾏假设。
(2) ⼀个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后⾯ 所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候, 都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存 储,可能暂时使⽤寄存器中的值,如果这个变量由别的程序更新了的话,将出现不⼀致的现 象。
(3)多线程应⽤中被⼏个任务共享的变量。
1.2 static关键字
static关键词的作⽤?
static是被声明为静态类型的变量,存储在静态区(全局区)中,其⽣命周期为整个程 序,如果是静态局部变量,其作⽤域为⼀对 {.. } 内,如果是静态全局变量,其作⽤域为当前 ⽂件。静态变量如果没有被初始化,则⾃动初始化为0。
为什么 static变量只初始化⼀次?
对于所有的对象(不仅仅是静态对象),初始化都只有⼀次,⽽由于静态变量具有“记 忆”功能,初始化后,⼀直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。 存放在静态区的变量的⽣命周期⼀般⽐较⻓,它与整个程序“同⽣死、共存亡”,所以它只 需初始化⼀次。⽽auto变量,即⾃动变量,由于它存放在栈区,⼀旦函数调⽤结束,就会 ⽴刻被销毁。
static修饰的全局变量,只能在本⽂件被调⽤;修饰的函数也只能在本⽂件调⽤。
补充:auto变量
- 作用域:只在声明它的代码块(通常是函数体或语句块)内可见。
- 存储期:当程序执行到它所在的作用域时,自动分配存储空间;当离开该作用域时,自动回收空间。
- 存储位置:一般分配在栈(stack)上。
- 关键字可省略:在 C/C++ 中,局部变量默认就是自动变量,所以写不写 auto 关键字都一样:
void foo() {
auto int x = 42; // 显式用 auto
int y = 13; // 隐式也是 auto
// x 和 y 都是自动变量
}
1.3 const关键字
(a)定义变量(局部变量或全局变量)为常量,例如:
const int a = 100; //定义一个常数
a = 50; //error,常量的值不能被修改
const int b; // error,常量在被定义的时候必须初始化
(b)修饰指针
//第一种
const int *p1; //常量指针,p本身不是const的,但是p指向的变量是const的
//第二种
int const *p2; //同第一种,const在*前
//第三种
int* const p3; //指针常量,p本身是一个const,但是p指向变量不是const
//第四种
const int* const p4; //p本身是一个const,而且p指向的变量是const。
第⼀种和第⼆种是常量指针;第三种是指针常量;第四种是指向常量的常指针。
(b1)⾯试问题1:什么是常量指针?
- 常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的⽅式 来改变变量的值的。
- 常量指针指向的值不能改变,但是这并不是意味着指针本⾝不能改变,常量指针 可以指向其他的地址。
const int *p;
int a = 12;
p = &a;
*p = 15;
printf(" p = %d \r\n " ,a);
return 0;
p1是定义的常量指针,p1指向a的地址,*p1 = 15是不⾏的,因为不能通过常 量指针去改变变量的值,如果去掉const则是可以的。
没有const时,利⽤*p1可以去对a的值进⾏修改,如下面代码:
int *p;
int a = 12;
p = &a;
*p = 15;
printf(" p = %d \r\n " ,a);
return 0;
(b2)⾯试问题2:什么是指针常量?
指针常量是指指针本⾝是个常量,不能在指向其他的地址,需要注意的是,指针常量指 向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向该地址的指针 来修改。
(b3)⾯试问题3:什么是指向常量的常指针?
是指针常量与常量指针的结合,指针指向的位置不能改变并且也不能通过这个指针改变 变量的值,但是依然可以通过其他的普通指针改变变量的值。
(c)const修饰函数的参数
表⽰在函数体内不能修改这个参数的值。
(d)修饰函数的返回值
(d1)如果给⽤const修饰返回值的类型为指针,那么函数返回值(即指针)的内容是 不能被修改的,⽽且这个返回值只能赋给被const修饰的指针。例如:
// 1. 定义一个函数,返回值类型是 const char*,
// 即“指向常量字符”的指针
const char * GetString()
{
// 比如这里返回一个字符串常量
return "Hello, world!";
}
// 2. 错误写法:把 const char*(只读)赋给 char*
// 会丢掉 const 属性,编译器会报错
char * str1 = GetString();
// 错误:从“const char*”转换到“char*”
// 丢弃了“const”限定符
// 3. 正确写法:接收端也要用 const char*
const char * str2 = GetString();
(d2)如果⽤const修饰普通的返回值,如返回int变量,由于这个返回值是⼀个临时变 量,在函数调⽤结束后这个临时变量的⽣命周期也就结束了,因此把这些返回值修饰为 const是没有意义的。
如果只是返回一个值(内置类型或自定义类型),返回类型上加 const没有意义。
1.4 typedef和 define有什么区别?
typedef与define都是替⼀个对象取⼀个别名,以此来增强程序的可读性,但是它们在 使⽤和作⽤上也存在着以下4个⽅⾯的不同。
(a)原理不同
-
#define是C语⾔中定义的语法,它是预处理指令,在预处理时进⾏简单⽽机械的字符 串替换,不做正确性检査,不管含义是否正确照样代⼊,只有在编译已被展开的源程序时, 才会发现可能的错误并报错。 例如, # define Pl3.1415926 ,当程序执⾏ area=Pr * r 语句 时,P会被替换为3.1415926。于是该 语句被替换为 area=3.1415926rr 。如果把# define 语句中的数字9写成了g,预处理也照样代⼊,⽽不去检查其是否合理、合法。
-
typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能。 它在⾃⼰的作⽤域内给⼀个已经存在的类型⼀个别名,但是不能在⼀个函数定义⾥⾯使⽤标识符 typedef。例如, typedef int INTEGER ,这以后就可⽤ INTEGER来代替int作整型变量的类型说明了,例 如:INTEGER a,b; ⽤ typedef定义数组、指针、结构等类型将带来很⼤的⽅便,不仅使程 序书写简单⽽且使意义更为明确,因⽽增强了可读性。例如: typedef int a[10]; 表⽰a是整 型数组类型,数组⻓度为10。然后就可⽤a说明变量,例如:语句a s1,s2;完全等效于语句 int s1[10],s2[10].同理, typedef void(*p)(void)表⽰p是⼀种指向void型的指针类型。
#include <iostream>
// 1. 定义 typedef
typedef void (*p)(void);
// 2. 一个符合签名的函数
void hello(void) {
std::cout << "Hello, world!\n";
}
int main() {
// 3. 用 p 来声明函数指针变量 fp
p fp = &hello; // 也可写成 p fp = hello;
// 4. 调用函数指针
fp(); // 等同于 hello();
return 0;
}
(b)功能不同
typedef⽤来定义类型的别名,这些类型不仅包含内部类型(int、char等),还包括⾃ 定义类型(如 struct),可以起到使类型易于记忆的功能。
例如: typedef int (PF)(const char *, const char) 定义⼀个指向函数的指针的数据 类型PF,其中函数返回值为int,参数为 const char*。typedef还有另外⼀个重要的⽤途, 那就是定义机器⽆关的类型。例如,可以定义⼀个叫REAL的浮点类型,在⽬标机器上它可 以获得最⾼的精度: typedef long double REAL ,在不⽀持 long double的机器上,该 typedef 看起来会是下⾯这样: typedef double real ,在 double都不⽀持的机器上,该 typedef看起来会是这样: typedef float REAL 。 #define不只是可以为类型取别名,还可 以定义常量、变量、编译开关等。
// 在支持 long double 的平台上:
typedef long double REAL;
// 如果平台不支持 long double,就改成:
// typedef double REAL;
// 如果还连 double 都不支持,就退到:
// typedef float REAL;
(c)作⽤域不同
#define没有作⽤域的限制,只要是之前预定义过的宏,在以后的程序中都可以使⽤, ⽽ typedef有⾃⼰的作⽤域。
下面这段代码演示了 #define 和 typedef 在作用域(可见性)上的差别:
void fun()
{
#define A int // 宏定义:在整个后续代码里都有效,直到被 #undef
}
void gun()
{
A x = 123; // OK:因为宏替换没有 C++ 作用域限制,A 会被替换成 int
}
void fun()
{
typedef int A; // typedef 只在其所在的作用域(这里是 fun 函数体内)有效
}
void gun()
{
A x = 123; // 错误:A 在 gun 函数里并不存在
}
(d)对指针的操作不同
#define INTPTR1 int*;
typedef int* INTPTR2;
INTPTR1 p1, p2;
INTPTR2 p3, p4;
-
INTPTR1 pl, p2和INTPTR2 p3,p4的效果截然不同。 INTPTR1 pl, p2进⾏字符串替换 后变成 int*p1,p2 ,要表达的意义是声明⼀个指针变量p1和⼀个整型变量p2。
-
INTPTR2 p3,p4,由于 INTPTR2是具有含义的,告诉我们是⼀个指向整型数据的指 针,那么p3和p4都为指针变量,这句相当于 intpl,p2 .从这⾥可以看出,进⾏宏替换是 不含任何意义的替换,仅仅为字符串替换;⽽⽤ typedef 为⼀种数据类型起的别名是带有⼀ 定含义的。
#define INTPTR1 int*
typedef int* INTPTR2;
int a = 1, b = 2, c = 3;
// 情况 1:const 加在宏替换上
const INTPTR1 p1 = &a;
// 情况 2:const 加在 typedef 别名上
const INTPTR2 p2 = &b;
// 情况 3:另一种写法,等价于情况 2
INTPTR2 const p3 = &c;
情况 1:const INTPTR1 p1
- 首先做宏替换:
const int* p1 = &a;
- 解释
- p1 是 “指向 常量 int 的指针” 常量指针
- 你不能通过 *p1 = … 去修改 a,但可以写 p1 = &someOtherInt; 把指针指向别处。
情况 2:const INTPTR2 p2
- INTPTR2 本身就是一个完整的指针类型(int*),再加 const,等价于
int * const p2 = &b;
- 解释:
- p2 是 “常量 指针,指向可变的 int”
- 你可以通过 *p2 = … 去修改 b,但不能写 p2 = &someOtherInt;(指针本身不可再赋值)。
情况 3:INTPTR2 const p3
- const 修饰 INTPTR2 的写法顺序可以前后互换,效果同“情况 2”:
2 变量、数组、指针
2.1变量
(a)定义常量谁更好?# define还是 const?
尺有所短,⼨有所⻓, define与 const都能定义常量,效果虽然⼀样,但是各有侧重。
define既可以替代常数值,⼜可以替代表达式,甚⾄是代码段,但是容易出错,⽽ const的引⼊可以增强程序的可读性,它使程序的维护与调试变得更加⽅便。具体⽽⾔,它 们的差异主要表现在以下3个⽅⾯。
(a1)define只是⽤来进⾏单纯的⽂本替换,define常量的⽣命周期⽌于编译期,不分 配内存空间,它存在于程序的代码段,在实际程序中,它只是⼀个常数;⽽const常量存在 于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实实存在,并且可以被 调⽤、传递。
(a2)const常量有数据类型,⽽define常量没有数据类型。编译器可以对const常量进 ⾏类型安全检査,如类型、语句结构等,⽽define不⾏。
(a3)很多IDE⽀持调试 const定义的常量,⽽不⽀持 define定义的常量由于const修饰 的变量可以排除 程序之间的不安全性因素,保护程序中的常量不被修改,⽽且对数据类型 也会进⾏相应的检查,极⼤地提⾼了程序的健壮性,所以⼀般更加倾向于⽤const来定义常 量类型。
(b)全局变量和局部变量的区别是什么?
(b1)全局变量的作⽤域为程序块,⽽局部变量的作⽤域为当前函数。
(b2)内存存储⽅式不同,全局变量(静态全局变量,静态局部变量)分配在全局数据 区(静态存储空间),后者分配在栈区。
- 静态局部变量不是“全局变量”,它只是跟全局变量一样都存放在静态区,不会进栈。
- 它最大的区别在于作用域受限(只能在本函数内)且无链接性,而真正的全局变量(即文件作用域变量)作用域更广,可以被其他函数甚至其他源文件(若有外部链接)访问。
(b3)⽣命周期不同。全局变量随主程序创建⽽创建,随主程序销毁⽽销毁,局部变量 在局部函数内部,甚⾄局部循环体等内部存在,退出就不存在了。
(b4)使⽤⽅式不同。通过声明为全局变量,程序的各个部分都可以⽤到,⽽局部变量 只能在局部使⽤。
(c)全局变量可不可以定义在可被多个.C⽂件包含的头⽂件中?为什么?
可以,在不同的C⽂件中以static形式来声明同名全局变量。
可以在不同的C⽂件中声明同名的全局变量,前提是其中只能有⼀个C⽂件中对此变量 赋初值,此时连接不会出错。
在头文件里直接写一个带初值的全局变量定义并被多个 .c
文件包含,默认会在每个翻译单元都产生一份同名的定义,最后在链接阶段必然报“multiple definition”错误。要想“在头文件里”又不出错,通常有两种做法:
## 1. 用 `static` 修饰,让它成为各自文件的“内部”全局
```c
// common.h
static int counter = 0;
```
* `static` 赋予它 **内部链接性**(internal linkage),即该变量对本翻译单元可见、对其它文件不可见。
* 每个包含了 `common.h` 的 `.c` 文件内,都会生成一份各自独立的 `counter`,互不冲突。
* 缺点是:它们根本不是同一个变量,彼此之间无法共享。
---
## 2. 在头文件 **声明**,在单个 `.c` 文件 **定义**
```c
// common.h
extern int counter; // 只是声明,不分配存储
// common.c
#include "common.h"
int counter = 0; // 真正的定义(带或不带初值,初次定义只能有一次)
```
* 任何引用 `counter` 的 `.c` 文件都只进行 `extern` 声明,不分配空间。
* 只有 `common.c` 里有一次真正的定义(带或不带初值都算“定义”),链接器因此只会看到一个存储实例,互相引用无误。
* 这是管理全局变量的推荐做法:**声明放在头文件,定义放在单个源文件**。
---
### 为什么不直接在头文件写非 `static` 定义?
* C 标准要求:
> 对同一个标识符的强定义(non-`extern` 定义)在整个程序中只能出现一次。
* 如果在头里写了 `int counter = 0;`,每个包含该头的 `.c` 都会生成一份定义,最终链接时就冲突了。
---
**总结:**
* **要让多个 `.c` 文件共享同一个全局变量**,在头文件里只写 `extern` 声明,在恰好一个源文件里写带初值的定义。
* **要让每个 `.c` 文件各自拥有一份同名“全局”**,可以在头里把它写成 `static`,这样每个翻译单元内部可见、互不干扰。
(d)局部变量能否和全局变量重名?
能,局部会屏蔽全局。
局部变量可以与全局变量同名,在函数内引⽤这个变量时,会⽤到同名的局部变量,⽽ 不会⽤到全局变量。 对于有些编译器⽽⾔,在同⼀个函数内可以定义多个同名的局部变 量,⽐如在两个循环体内都定义⼀个同名的局部变量,⽽那个局部变量的作⽤域就在那个循 环体内。
#include <stdio.h>
int x = 100; // 全局变量
void func(void) {
int x = 10; // 局部变量,屏蔽了全局的 x
printf("func 中 x = %d\n", x); // 打印 10
for (int i = 0; i < 1; ++i) {
int x = 20; // 块级作用域内,又定义了同名局部变量
printf("循环内部 x = %d\n", x); // 打印 20
}
printf("循环外的 x = %d\n", x); // 仍然是最外层局部的 x,打印 10
}
int main(void) {
func();
printf("main 中全局 x = %d\n", x); // 访问全局变量 x,打印 100
return 0;
}
2.2 数组
(a)数组指针
数组指针就是指向数组的指针,它表示的是一个指针,这个指针指向的是一个数组,它的重点是指针。 例如, int(*pa)[8] 声明了一个指针,该指针指向了一个有8个int型元素的数组。下面给出一个数组 指针的示例。
#include <stdio.h>
#include <stdlib.h>
void main() {
int b[12]={1,2,3,4,5,6,7,8,9,10,11,12};
int (*pa)[4]; // 声明一个指向数组的指针,指向一个有4个int元素的数组
pa = b; // 将数组b强制转换为指向4个int元素的数组的指针
printf("%d\n", **(++pa); // 输出数组b的第一个元素的值
}
程序的输出结果为 5。
上例中,p是一个数组指针,它指向一个包含有4个int类型数组的指针,刚开始p被初始化为指向数组b 的首地址,++p相当于把p所指向的地址向后移动4个int所占用的空间,此时p指向数组{5,6,7,8},语句 *(++p); 表示的是这个数组中第一个元素的地址(可以理解p为指向二维数组的指针,{1,2,3,4}, {5,6,7,8},{9,10,11,12}。p指向的就是{1,2,3,4}的地址, *p 就是指向元素,{1,2,3,4}, p 指向的就是1,语句(++p)会输出这个数组的第一个元素5。
二级解引用:**pa
- *pa:把 pa 当作指针解一次,得到它所指向的整个「4 元素数组」——也就是一个类型为 int[4] 的左值,具体就是那一组 {5,6,7,8}。
- **pa:再把上一步的结果(数组)当作指针,用第一个元素去解引用,等于那个子数组的第 0 个元素,也就是 5。
(b)指针数组
指针数组是一个数组,数组的每个元素都是一个指针。它的重点是数组。 例如, int *pa[8] 声明了一个数组,该数组包含8个int型指针。下面给出一个指针数组的示例。
int main()
{
int i;
int *p[4]; // 声明一个指针数组,包含4个int型指针
int a[4] = {1, 2, 3, 4}; // 定义一个包含4个int元素的数组
p[0] = &a[0]; // 将数组a的第一个元素的地址赋给p[0]
p[1] = &a[1]; // 将数组a的第二个元素的地址赋给p[1]
p[2] = &a[2]; // 将数组a的第三个元素的地址赋给p[2]
p[3] = &a[3]; // 将数组a的第四个元素的地址赋给p[3]
for (i = 0; i < 4; i++) {
printf("%d ", *p[i]); // 输出指针数组中每个指针所指向的值
}
printf("\n");
return 0;
}
程序的输出结果为1234。
(c)数组指针和指针数组的区别
-
定义方式
- 数组指针:
int (*pa)[8];
- 指针数组:
int *pa[8];
- 数组指针:
-
存储内容
- 数组指针:存的是整个数组的首地址(一个指针)
- 指针数组:存的是多个指针,各自指向不同的
int
-
访问元素
- 数组指针:
(*pa)[i]
或*(*pa + i)
- 指针数组:
*(pa[i])
或pa[i][0]
、pa[i][j]
- 数组指针:
-
典型用途
- 数组指针:遍历或传递 整个 定长数组(如二维数组行)
- 指针数组:管理多条独立数据(如数个字符串、函数指针列表)
-
内存分配
- 数组指针:指针本身在栈上,其指向的数组通常静态或在栈上分配
- 指针数组:数组也在栈上,但每个指针可指向堆上或其他任意内存
-
初始化
- 数组指针:
int a[8]; int (*pa)[8] = &a;
- 指针数组:
int x,y; int *pa[2] = { &x, &y };
- 数组指针:
(d)数组下标可以为负数吗?
可以,因为下标只是给出了一个与当前地址的偏移量而已,只要根据这个偏移量能定位得到目标地址即可。
#include <stdio.h>
int main(void) {
int a[5] = { 10, 20, 30, 40, 50 };
int *p = &a[2]; // p 指向 a[2] (值为 30)
// p[-1] 等价于 *(p - 1),即访问 a[1]
printf("p[-1] = %d\n", p[-1]); // 输出 20
printf("p[ 0] = %d\n", p[ 0]); // 输出 30
printf("p[ 1] = %d\n", p[ 1]); // 输出 40
return 0;
}
2.3 指针
(a)函数指针