Skip to content

1、指针数组与数组指针的区别

  1. 指针数组:

指针数组是指一个数组,其元素都是指针。每个元素指向一个独立的数据对象。在声明时,注意使用 * 表示元素为指针。

C++
int *ptrArr[5];  // 声明一个包含 5 个指向整数的指针的数组
  1. 数组指针:

数组指针是指一个指针,它指向一个数组。在声明时,注意使用 [] 表示指向数组的指针。

C++
int arr[5];
int (*ptrArr)[5] = &arr;  // 声明一个指向包含 5 个整数的数组的指针

区别:

  • 元素类型不同:
    • 指针数组的元素是指针。
    • 数组指针指向一个数组。
  • 声明语法不同:
    • 指针数组的声明中,数组的元素是指针,需要在声明时使用 *
    • 数组指针的声明中,指针指向数组,需要在声明时使用 []
  • 用法不同:
    • 指针数组用于存储一组指针,每个指针可以指向不同类型的对象。
    • 数组指针用于指向一个数组,通常用于处理多维数组。

2、指针和引用的区别

  1. 定义和声明:
  • 指针:
C++
int *ptr;  // 声明一个指向整数的指针
  • 引用:
C++
int x = 10;
int &ref = x;  // 声明一个整数引用,绑定到变量 x
  1. 内存地址:
  • 指针:
    • 存储的是变量的地址。
    • 可以为空(nullptr)或指向不同的变量。
  • 引用:
    • 引用本身并不占用额外的内存。
    • 引用必须在声明时初始化,并且绑定到相应的变量。
  1. 空值:
  • 指针:
    • 可以是空指针,即指向空地址。
  • 引用:
    • 不存在空引用,必须在初始化时绑定到一个变量。
  1. 重新赋值:
  • 指针:
    • 可以改变指向的变量。
C++
int x = 5, y = 10;
int *ptr = &x;
ptr = &y;  // ptr 现在指向 y
  • 引用:
    • 一旦绑定,就不能再绑定到其他变量。
C++
int x = 5, y = 10;
int &ref = x;
// ref = y;  // 错误,引用一旦绑定不能改变
  1. 操作符:
  • 指针:
    • 使用 * 运算符用于间接访问所指向的值。
C++
int x = 5;
int *ptr = &x;
int value = *ptr;  // value 等于 5
  • 引用:
    • 不需要使用 * 运算符,直接使用引用变量即可。
C++
int x = 5;
int &ref = x;
int value = ref;  // value 等于 5
  1. NULL指针:
  • 指针:
    • 可以设置为 nullptr 表示空指针。
C++
int *ptr = nullptr;
  • 引用:
    • 引用不可为空,必须在初始化时指向一个已存在的变量。
  1. 数组:
  • 指针:
    • 可以指向数组,并通过指针进行数组操作。
C++
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
  • 引用:
    • 不能直接绑定到数组,但可以通过引用数组元素。
C++
int arr[5] = {1, 2, 3, 4, 5};
int &ref = arr[0];

总结:

  • 指针是一个独立的对象,可以在运行时改变它的值,可以为空。
  • 引用是一个别名,必须在初始化时指向一个已存在的对象,不能为空,一旦绑定后不能再绑定到其他对象。

3、C语言几种传值方式,区别

  1. 传值调用:

在传值调用中,函数参数的值被复制给形参。这意味着函数内对形参的修改不会影响到实参的值。

C++
void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(x, y);
    // x 和 y 的值没有变化
    return 0;
}
  1. 传引用调用(Call by Reference):

在传引用调用中,函数参数是实参的引用或地址,通过指针或引用传递。这样,函数内对形参的修改会影响到实参的值。

使用指针:

C++
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y);
    // x 和 y 的值被交换
    return 0;
}

使用引用(C++):

C++
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(x, y);
    // x 和 y 的值被交换
    return 0;
}

区别总结:

  • 传值调用:
    • 形参是实参的拷贝,函数内对形参的修改不影响实参。
    • 适用于不希望修改实参的情况。
    • 简单、安全,但可能会有性能开销。
  • 传引用调用:
    • 形参是实参的引用或地址,函数内对形参的修改影响实参。
    • 适用于希望修改实参的情况,能够节省内存和提高效率。
    • 可能引发错误,需要谨慎使用。

4、指针函数与函数指针的区别

  1. 指针函数:

指针函数是指一个返回指针的函数。在声明和定义时,它的语法如下:

C
int myFunction(int param1, char param2) {
    // 函数体
}

这里 int* 表示函数返回一个整数指针。

  1. 函数指针:

函数指针是指一个指向函数的指针变量。在声明和定义时,它的语法如下:

C
int myFunction(int param1, char param2) {
    // 函数体
}

int (*ptrToFunction)(int, char) = &myFunction;  // 函数指针的声明和定义

这里 int (*ptrToFunction)(int, char) 表示声明了一个指向函数的指针变量 ptrToFunction,该函数接受一个整数和一个字符作为参数,并返回一个整数。

区别总结:

  1. 返回类型不同:
    • 指针函数是一个返回指针的函数。
    • 函数指针是一个指向函数的指针变量。
  2. 用法不同:
    • 指针函数是一个函数,可以调用并返回指针。
    • 函数指针是一个指向函数的指针变量,可以通过该变量调用相应的函数。
  3. 语法不同:
    • 指针函数的语法类似于普通函数的声明和定义,只是返回类型是指针类型。
    • 函数指针的语法涉及到指针的声明和定义,需要指明函数的参数类型和返回类型。

5、malloc和calloc的区别

  1. 参数个数和类型:
  • malloc

    • C++
      void* malloc(size_t size);
    • 接受一个参数,即要分配的字节数。

    • 返回一个指向分配内存起始位置的指针。

  • calloc

    • C++
      void* calloc(size_t num_elements, size_t element_size);
    • 接受两个参数,分别是要分配的元素数量和每个元素的字节数。

    • 返回一个指向分配内存起始位置的指针。

  1. 内存内容:
  • malloc
    • 分配的内存内容是未初始化的,可能包含任意值。
    • 使用 malloc 分配的内存需要使用 memset 或其他手段进行初始化。
  • calloc
    • 分配的内存内容被初始化为零(所有位都是0)。
    • 适用于需要确保内存内容为零的情况。
  1. 出错处理:
  • malloc
    • 如果分配失败,返回 NULL
    • 需要检查返回值以确保分配成功。
  • calloc
    • 如果分配失败,返回 NULL
    • 同样需要检查返回值以确保分配成功。

使用示例:

  • malloc 示例:
C++
int *arr = (int*)malloc(5 * sizeof(int));
  • calloc 示例:
C++
int *arr = (int*)calloc(5, sizeof(int));

总结:

  • malloc 用于分配指定字节数的内存,内容未初始化。
  • calloc 用于分配指定数量和大小的元素的内存,内容被初始化为零。
  • 在使用 malloc 分配内存后,可能需要手动初始化内存内容。
  • 在使用 calloc 分配内存后,可以确保内存内容为零。
  • 在检查内存分配是否成功时,都需要检查返回值是否为 NULL

6、原码、反码、补码

  1. 原码(Sign-Magnitude Representation):

原码是最直观的整数表示方式,其中最高位表示符号位(0表示正数,1表示负数),其余位表示数字的绝对值。例如:

  • 5 的原码:00000101
  • -5 的原码:10000101
  1. 反码(Ones' Complement):

反码是在原码的基础上,负数的表示方式取反。正数的反码和原码相同。例如:

  • 5 的反码:00000101
  • -5 的反码:11111010
  1. 补码(Two's Complement):

补码是在反码的基础上,再加1。补码的优势在于它只有一个零,而且加法和减法可以使用相同的硬件电路。正数的补码和原码相同。例如:

  • 5 的补码:00000101
  • -5 的补码:11111011

补码的性质:

  1. 加法和减法一致: 在计算机中,加法和减法使用相同的硬件电路,无需额外的减法电路。
  2. 唯一的零表示: 补码中只有一个零表示,避免了正零和负零的问题。
  3. 范围表示: 补码表示范围为 −2^(n−1),2^(n−1)−1,其中 n 是位数。
  4. 溢出处理: 溢出的结果可以被忽略,不会导致错误的计算结果。

7、内联函数和函数的区别

  1. 内联函数(Inline Function):
  • 定义: 内联函数是在函数声明前面加上 inline 关键字的函数,通常在头文件中定义。编译器会尝试将内联函数的代码插入到每个调用它的地方,而不是通过正常的函数调用机制。
  • 特点:
    • 提高代码执行效率,减少函数调用开销。
    • 适用于短小的函数,避免频繁的函数调用带来的开销。
  • 示例:
C++
inline int add(int a, int b) {
    return a + b;
}
  1. 普通函数:
  • 定义: 普通函数是一般的函数,不带 inline 关键字。函数调用时,会按照正常的函数调用机制执行,包括压栈、跳转、返回等步骤。
  • 特点:
    • 函数调用会有一定的开销,包括压栈、跳转、返回等操作。
    • 适用于较大或者复杂的函数,不适合频繁调用。
  • 示例:
C++
int add(int a, int b) {
    return a + b;
}

区别:

  1. 执行效率:
    • 内联函数的执行效率通常更高,因为避免了函数调用的开销。
    • 普通函数的执行效率受到函数调用的开销影响。
  2. 代码体积:
    • 内联函数可能会导致代码体积增大,因为它的代码会被复制到每个调用它的地方。
    • 普通函数的代码只有一份,不会被复制,因此不会导致代码体积增大。
  3. 适用场景:
    • 内联函数适用于短小的函数,特别是在循环内部或频繁调用的场景。
    • 普通函数适用于较大或复杂的函数,或者需要在多个地方调用的场景。
  4. 编译器优化:
    • 编译器有权选择是否真正内联函数,inline 关键字只是一个建议,不是强制要求。编译器可能会根据具体情况进行优化。
    • 普通函数不会被编译器自动内联。

8、大端对齐与小端对齐

  • 大端对齐:数据的低位存储在内存的高地址上,数据高位存储存储在内存的低地址上;(字符串存储)。
  • 小端对齐:数据的低位存储在内存的低地址上,数据高位存储存储在内存的高地址上。

9、常量指针与指针常量的区别

  1. 常量指针

定义:具有只能够读取内存中数据,却不能够修改内存中数据的属性的指针,称为指向常量的指针,简称常量指针。

声明:const int * p; int const * p;

:可以将一个常量的地址赋值给一个对应类型的常量指针,因为常量指针不能够通过指针修改内粗数据。只能防止通过指针引用修改内存中的数据,并不保护指针所指向的对象。

  1. 指针常量

定义:指针常量是指指针所指向的位置不能改变,即指针本身是一个常量,但是指针所指向的内容可以改变。

声明:int * const p=&a;

:指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的。

  1. 内联函数比普通函数多了关键字inline。
  2. 内联函数避免了函数调用的开销;普通函数有调用的开销。
  3. 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
  4. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。

10、C语言中都有哪些数据结构

  1. 结构体(Structures):

    struct 关键字用于定义自定义数据结构,可以包含多个不同类型的成员。

C
struct Point {
    int x;
    int y;
};
  1. 数组(Arrays):

​ 数组是一组相同类型的元素的集合。

C
int numbers[5] = {1, 2, 3, 4, 5};
  1. 链表(Linked Lists):

​ 通过结构体和指针实现的动态数据结构,可以表示一系列相邻的元素。

C
struct Node {
    int data;
    struct Node* next;
};
  1. 栈(Stacks):

​ 具有后进先出(LIFO)特性的数据结构。

C
#define MAX_SIZE 100
struct Stack {
    int items[MAX_SIZE];
    int top;
};
  1. 队列(Queues):

​ 具有先进先出(FIFO)特性的数据结构。

C
#define MAX_SIZE 100
struct Queue {
    int items[MAX_SIZE];
    int front, rear;
};
  1. 树(Trees):

​ 通过结点和链接来表示的层次结构,包括二叉树、二叉搜索树等。

C
struct TreeNode {
    int data;
    struct TreeNode* left;
    struct TreeNode* right;
};
  1. 图(Graphs):

​ 由顶点和边组成的集合,可以用邻接矩阵或邻接表表示。

C
#define MAX_VERTICES 100
struct Graph {
    int vertices;
    int matrix[MAX_VERTICES][MAX_VERTICES];
};
  1. 哈希表(Hash Tables):

​ 使用散列函数将键映射到存储位置的数据结构。

C
#define TABLE_SIZE 100
struct HashTable {
    int keys[TABLE_SIZE];
    int values[TABLE_SIZE];
};

11、变量的声明与定义有什么区别

声明:

  • 定义: 变量的声明是指在程序中引入一个变量的名称,告诉编译器该变量的存在和类型,但并不分配实际的存储空间。
  • 关键字: 使用关键字 extern 或者不使用任何关键字,只是简单地声明变量的类型和名称。
  • 示例:
C
extern int x;  // 只是声明变量 x 的存在,不分配内存空间

定义:

  • 定义: 变量的定义是指在程序中分配实际的存储空间,同时声明变量的名称和类型。
  • 关键字: 使用关键字 int(或其他数据类型),同时初始化变量。
  • 示例:
C
int x = 10;  // 定义并初始化变量 x,分配内存空间

区别:

  1. 内存分配:
    • 声明: 不分配实际的内存空间,只是告诉编译器该变量的类型和名称。
    • 定义: 分配实际的内存空间,并告诉编译器该变量的类型和名称。
  2. 关键字:
    • 声明: 可以使用关键字 extern 或不使用关键字。
    • 定义: 使用变量的数据类型关键字(如 intfloat)。
  3. 初始化:
    • 声明: 不进行初始化。
    • 定义: 可以进行初始化。
  4. 多次声明:
    • 声明: 可以多次声明同一变量,但只能定义一次。
    • 定义: 只能定义一次,但可以多次声明。

12、内联函数和宏函数的区别

  1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
  2. 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
  3. 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等。

13、左值引用和右值引用的区别

  1. 左值引用

T & a = 引用对象,显示的声明且初始化后,就相当于一个变量,由于拥有变量的相同的地址,使用也是与变量一样。

  1. 右值引用

T && a = 被引用的对象,一般不是显式的定义,用于传参和返回,与左值引用的区别,右值是一个临时变量且不变的,变量的所有权会转移,可以理解为,右值引用初始化后,被引用的对象消失,后面一般不再使用。

14、32位或64位平台下指针的大小

  1. 32位平台:

    在32位平台上,一个指针通常占用4字节(32位),因为它需要存储一个32位的内存地址。

  2. 64位平台:

    在64位平台上,一个指针通常占用8字节(64位),因为它需要存储一个64位的内存地址。

在计算机内存中,每个地址单元都有一个唯一的地址,而指针的作用就是存储这个地址。在32位系统中,2^32 个不同的地址,而在64位系统中,有2^64 个不同的地址。因此,为了能够寻址整个内存空间,指针的大小需要足够大。

15、哪些情况下会出现野指针

  1. 释放后未置空:

    当使用 free 函数释放了内存后,如果没有将指针置为 NULL,指针仍然包含先前分配的内存地址,成为野指针。

C
int *ptr = (int*)malloc(sizeof(int));
free(ptr);
// 在释放后未将 ptr 置为 NULL
  1. 指针未初始化:

​ 当一个指针被声明但未初始化时,它的值是不确定的,可能包含任意地址,成为野指针。

C
int *ptr;
// ptr 没有被初始化,包含不确定的地址
  1. 函数返回局部变量地址:

​ 当一个函数返回一个局部变量的地址时,该指针在函数调用结束后就成为野指针。

C
intgetLocalPointer() {
    int x = 10;
    return &x;  // 返回局部变量的地址
}
  1. 指针越界访问:

​ 当使用指针访问超出分配内存范围的地址时,可能导致野指针。

C
int *arr = (int*)malloc(5sizeof(int));
arr[5] = 10;  // 越界访问
  1. 二次释放:

​ 当同一个指针被多次释放时,会导致野指针。

C
cCopy code
int *ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr);  // 二次释放
  1. 指针拷贝问题:

​ 当两个指针指向相同的内存区域,其中一个指针释放了内存,而另一个指针仍然在使用,就可能导致野指针。

C
int *ptr1 = (int*)malloc(sizeof(int));
int *ptr2 = ptr1;
free(ptr1);
*ptr2 = 10;  // 使用了已经释放的内存

避免野指针的最佳实践是在使用指针后将其置为 NULL,并确保不要访问已释放的内存。使用动态内存时,要注意内存的生命周期,确保在使用指针时内存仍然有效。

16、浅拷贝与深拷贝

浅拷贝:

在C语言中,浅拷贝通常是通过将一个结构体或数组的内容复制到另一个结构体或数组中来实现的。这只涉及到对结构体或数组的直接成员的复制,而不涉及到成员可能指向的动态分配内存的内容。

C++
#include <stdio.h>
#include <string.h>

struct Person {
    char name[20];
    int age;
};

int main() {
    struct Person person1 = {"Alice", 25};
    struct Person person2;

    // 浅拷贝,直接将结构体内容复制到另一个结构体
    person2 = person1;

    // 修改 person2 的属性会影响 person1
    strcpy(person2.name, "Bob");

    // 输出 "Bob"
    printf("%s\n", person1.name);

    return 0;
}

深拷贝:

在C语言中,深拷贝通常涉及到手动管理动态分配内存,并将内容复制到新的内存空间。这需要使用 mallocfree 函数来管理内存。

C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Person {
    char *name;
    int age;
};

// 函数实现深拷贝
struct Person deepCopy(struct Person original) {
    struct Person copy;
    copy.name = malloc(strlen(original.name) + 1);  // +1 用于存储字符串结束符 '\0'
    strcpy(copy.name, original.name);
    copy.age = original.age;
    return copy;
}

int main() {
    struct Person person1 = {"Alice", 25};
    struct Person person2;

    // 深拷贝,使用函数实现
    person2 = deepCopy(person1);

    // 修改 person2 的属性不会影响 person1
    free(person2.name);  // 注意释放内存
    person2.name = malloc(strlen("Bob") + 1);
    strcpy(person2.name, "Bob");

    // 输出 "Alice"
    printf("%s\n", person1.name);

    return 0;
}

17、静态函数与普通函数的区别

  1. 生命周期和作用域:
  • 静态函数:
    • 静态函数具有文件作用域,只在定义它的源文件中可见。这意味着其他源文件无法调用该静态函数。
    • 静态函数在程序的整个生命周期内保持存在,但只能在定义它的源文件中调用。
  • 普通函数:
    • 普通函数具有全局作用域,可以在任何地方调用,只要它们的声明可见。
    • 普通函数的生命周期取决于程序的执行流程,只有在函数被调用时才会存在。
  1. 可见性:

    • 静态函数:仅在定义它的源文件中可见。其他源文件无法直接调用或访问静态函数。

    • 普通函数:可以被其他源文件调用,只要它们的声明(函数原型)在调用之前是可见的。

  2. 编译单元和链接:

    • 静态函数:仅在定义它的编译单元(通常是一个源文件)中可见。在链接阶段,其他编译单元无法访问静态函数。

    • 普通函数:在链接阶段对所有编译单元可见,可以在不同的编译单元中调用。

  3. 函数名的重用:

    • 静态函数:不同的源文件可以定义相同名称的静态函数,而不会发生冲突。每个静态函数只在自己的源文件中可见。

    • 普通函数:不同的源文件定义相同名称的普通函数可能导致链接错误,因为链接器无法确定哪个函数应该被调用。