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)
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变量

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)原理不同

#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;
#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

情况 2:const INTPTR2 p2

情况 3:INTPTR2 const p3


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

(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)数组指针和指针数组的区别

(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)函数指针