C/C++面试八股题(九)
目录:
1.请你简单说说数组存放在哪里?
2.请你介绍一下c/c++中变量的作用域都有哪些?
3.请问sizeof与strlen的区别,简单说说?
4.简述C++有几种传值方式之间的区别?
5.指针用的多不,请问数组指针与指针数组的区别?
6.请问NULL和nullptr区别?
内容:
1.请你简单说说数组存放在哪里?
栈
- 当数组是作为局部变量定义在函数中时,数组会存放在栈中。
- 栈中的数组内存是自动分配和释放的,由编译器完成。
- 数组在函数结束后,栈上的内存会被回收。
- 栈的大小是有限的,通常在几 MB 左右,不能分配太大的数组。
例子
void func() { int* arr = (int*)malloc(sizeof(int) * 10); // 动态分配,存放在堆中 arr[0] = 42; free(arr); // 释放堆内存 }
堆
- 当使用动态分配(如
malloc
、calloc
或new
)创建数组时,数组存放在堆中。 - 堆中的数组内存是手动分配和释放的,需要程序员显式调用
free
或delete
释放内存。 - 堆的大小一般远大于栈,适合分配较大的数组。
例子:
void func() { int* arr = (int*)malloc(sizeof(int) * 10); // 动态分配,存放在堆中 arr[0] = 42; free(arr); // 释放堆内存 }
数据段
- 数据段分为 全局/静态区 和 BSS段,具体取决于变量是否被初始化。
- 如果数组是全局变量,或者被
static
修饰且已初始化,会存储在 数据段的已初始化区。 - 程序运行时全程有效。
- 这些数组会在程序加载时分配,并且内存初始化为指定的值。
int arr[3] = {1, 2, 3}; // 全局数组,存储在数据段的已初始化区 static int s_arr[3] = {4, 5, 6}; // 静态数组,存储在数据段的已初始化区
- 如果数组是全局变量或被
static
修饰且未初始化,则存储在 BSS段。 - BSS段的数组会在程序加载时分配,并自动初始化为全 0。
int arr[3]; // 全局数组,未初始化,存储在BSS段 static int s_arr[3]; // 静态数组,未初始化,存储在BSS段
代码段
- 如果数组是用
const
修饰的,并且是只读数据(常量数组),通常存放在 代码段的只读区 或 常量区。 - 这些数组是只读的,不能被修改。
const char str[] = "Hello, world!"; // 字符常量数组,可能存储在代码段或常量区
总结
局部数组(自动分配) | 栈 |
动态分配的数组 | 堆 |
全局/静态数组(已初始化) | 数据段(已初始化区) |
全局/静态数组(未初始化) | 数据段(BSS段) |
修饰的只读数组 | 代码段或常量区 |
2.请你介绍一下c/c++中变量的作用域都有哪些?
块作用域
- 变量在某个代码块
{}
内声明,只在该代码块中可见,出了代码块就无法访问。 - 代码块内的变量是局部变量。
- 代码块外无法访问代码块内的变量。
- 变量作用域以
{
开始,以}
结束。 - 在函数、循环、条件语句等代码块中定义的变量。
例子:
void example() { int x = 10; // x 的作用域从这里开始 { int y = 20; // y 的作用域从这里开始 printf("x = %d, y = %d\n", x, y); } // printf("%d\n", y); // 错误,y 超出作用域 }
函数作用域
- 函数作用域表示变量仅在函数内可见。变量在函数内部声明,只能在同一函数内访问。函数作用域的变量在函数执行时创建,在函数结束时销毁。
例子:
void func1() { int x = 10; // x 的作用域只在 func1 内 } void func2() { int y = 20; // y 的作用域只在 func2 内 // printf("%d\n", x); // 错误,x 不在作用域内 }
全局作用域
- 全局作用域表示变量在整个程序中都可见。在任何函数或代码块之外声明的变量都具有全局作用域。这些变量在程序开始执行时创建,在程序结束时销毁。
例子:
#include <iostream> int x = 10; // 全局变量 int main() { std::cout << x << std::endl; // 输出:10 return 0; }
在上面的例子中,变量x在main函数中被访问,因为它具有全局作用域。
类作用域
- 在类或结构体中声明的变量,其作用域限制在类的范围内。
- 成员变量和成员函数都属于类作用域。
- 私有成员(
private
)只能在类内部访问。 - 公共成员(
public
)可以通过类对象访问。 - 使用
::
作用域解析符可以访问静态成员变量或成员函数。
例子:
class MyClass { private: int private_var; // 私有成员,仅在类内部可见 public: int public_var; // 公有成员,可以通过对象访问 void display() { private_var = 10; // 类内访问私有成员 std::cout << "Private Var: " << private_var << std::endl; } }; int main() { MyClass obj; obj.public_var = 20; // 可以访问公有成员 // obj.private_var = 10; // 错误,私有成员不可直接访问 }
命名空间作用域
- 在命名空间中定义的变量,其作用域限制在命名空间内。
- 通过
namespace
定义。 - 命名空间中的变量和函数不会与其他命名空间冲突。
- 可以通过
using namespace
或namespace::name
来访问。
例子:
namespace MyNamespace { int x = 10; // 命名空间作用域 } int main() { std::cout << MyNamespace::x << std::endl; // 使用作用域解析符 return 0; }
函数参数的作用域
- 函数参数也具有自己的作用域,它们在函数内部有效。函数参数作用域指的是这些参数在整个函数内部可见。
例子:
int x = 10; // 全局变量 void func(int x) { // 函数参数 printf("%d\n", x); // 输出函数参数 x 的值 } int main() { func(20); // 输出 20 return 0; }
动态作用域
- 动态分配的变量(如
malloc
、new
)在内存堆中分配,不受函数或代码块的限制,但需要手动释放。 - 动态分配的内存只要不释放,可以在程序中任何地方访问。
- 通常需要使用指针管理。
- 不释放会导致内存泄漏。
例子:
int* allocate() { int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存 *ptr = 10; return ptr; } int main() { int* p = allocate(); printf("%d\n", *p); // 输出 10 free(p); // 释放内存 return 0; }
3.请问sizeof与strlen的区别,简单说说?
sizeof
- 定义: 用于获取数据类型或对象的内存大小(单位是字节)。
- 编译器在编译阶段直接计算,不会在运行时执行。
- 计算包括结构体、数组(静态分配)、基本数据类型等所占用的完整内存大小。
- 对于数组,它返回数组的总大小,而不是元素数量。
语法:
sizeof(expression or type)
strlen
- 定义: 用于计算以
'\0'
结尾的字符串的长度(不包括末尾的'\0'
)。 - 只能用于以
'\0'
结尾的字符串。 - 计算时从起始地址开始遍历字符,直到遇到
'\0'
,因此是运行时操作。 - 不适用于非字符串数据。
语法:
strlen(const char *str)
sizeof
- 获取类型大小:
- 对静态数组:
- 对动态分配的指针:
strlen
- 获取字符串长度:
- 使用在指针字符串上:
总结
用途 | 用于计算数据类型或变量的内存大小。 | 用于计算以 |
运行时间 | 编译时确定(静态)。 | 运行时计算(动态)。 |
作用对象 | 任意类型,包括数组、指针、结构体等。 | 仅适用于 C 风格字符串(以 |
单位 | 返回字节数。 | 返回字符串中的字符数(不包含 |
数组的行为 | 对数组返回整个数组的大小(单位是节)。 | 对字符串数组返回实际字符串的长度。 |
例子
sizeof
和 strlen
结合使用
#include <stdio.h> #include <string.h> int main() { char arr[] = "Hello, World!"; char *ptr = "Hello, World!"; printf("sizeof(arr): %zu\n", sizeof(arr)); // 包括 '\0',输出14 printf("strlen(arr): %zu\n", strlen(arr)); // 不包括 '\0',输出13 printf("sizeof(ptr): %zu\n", sizeof(ptr)); // 指针大小,输出8(假设指针是8字节) printf("strlen(ptr): %zu\n", strlen(ptr)); // 指向字符串的长度,输出13 return 0; }
输出:
sizeof(arr): 14 strlen(arr): 13 sizeof(ptr): 8 strlen(ptr): 13
4.简述C++有几种传值方式之间的区别?
值传递
- 将实参的值复制一份传递给形参。
- 形参和实参是两个不同的变量,形参的修改不会影响实参。
例子:
void func(int x) { x = 10; // 只修改了形参,不会影响实参 } int main() { int a = 5; func(a); // a 仍然是 5 return 0; }
指针传递
- 将实参的地址传递给形参(指针)。
- 通过形参指针可以访问和修改实参的值。
例子:
void func(int *x) { *x = 10; // 修改了实参的值 } int main() { int a = 5; func(&a); // 传递 a 的地址 // a 变为 10 return 0; }
引用传递
- 将实参的引用传递给形参。
- 实参和形参共享同一块内存,修改形参会直接影响实参。
例子:
void func(int &x) { x = 10; // 修改了实参的值 } int main() { int a = 5; func(a); // 传递 a 的引用 // a 变为 10 return 0; }
总结
值传递 | 复制实参值,修改形参不影响实参。 | 基本数据类型,小型数据,不需要修改实参。 |
指针传递 | 传递地址,允许修改实参,支持空指针,使用较复杂。 | 需要修改实参或传递大数据,灵活但需注意安全性。 |
引用传递 | 实参和形参共享,允许修改实参,语法简单,不能传 | 需要修改实参,代码简洁,适合对对象的操作。 |
5.指针用的多不,请问数组指针与指针数组的区别?
数组指针
- 数组指针是指向数组的指针,它指向的是整个数组的首地址。其定义方式通常是:
类型 (*指针名)[数组大小]
。
特点:
- 一个数组指针可以指向一个特定大小的数组。
- 通过数组指针,可以遍历整个数组。
例子:
int arr[5] = {1, 2, 3, 4, 5}; // 定义一个数组 int (*p)[5] = &arr; // 定义一个数组指针,指向数组 arr // 使用数组指针访问数组元素 for (int i = 0; i < 5; ++i) { std::cout << (*p)[i] << " "; // 通过数组指针解引用访问元素 }
p
是一个指针,指向一个int
类型的数组,该数组有 5 个元素。*p
解引用后就是数组本身,(*p)[i]
就是数组中的第i
个元素。
指针数组
- 指针数组是一个数组,数组的每个元素是一个指针。定义方式通常是:
类型 *数组名[数组大小]
。
特点:
- 一个指针数组包含多个指针,可以分别指向不同的对象或变量。
- 指针数组的每个元素可以存储一个地址。
例子:
int a = 10, b = 20, c = 30; int *arr[3] = {&a, &b, &c}; // 定义一个指针数组,每个元素是指向 int 的指针 // 使用指针数组访问变量的值 for (int i = 0; i < 3; ++i) { std::cout << *arr[i] << " "; // 解引用指针,获取值 }
arr
是一个数组,数组中存放的是指向int
类型变量的指针。arr[i]
表示数组中第i
个指针,*arr[i]
表示该指针指向的变量的值。
对比
数组指针的函数传参:
void func(int (*p)[5]) { for (int i = 0; i < 5; ++i) { std::cout << (*p)[i] << " "; } } int main() { int arr[5] = {1, 2, 3, 4, 5}; func(&arr); // 将数组的地址传递给数组指针 return 0; }
指针数组操作字符串:
int main() { const char *strs[3] = {"Hello", "World", "C++"}; // 指针数组,存储字符串地址 for (int i = 0; i < 3; ++i) { std::cout << strs[i] << std::endl; // 打印每个字符串 } return 0; }
区别总结
定义 |
|
|
含义 | 指针指向一个特定大小的数组。 | 数组的每个元素是一个指针,指向不同的对象或变量。 |
用途 | 操作整个数组。 | 操作多个指针,每个指针可以指向不同的变量或数组。 |
内存结构 | 单个指针指向数组的首地址。 | 数组中存放多个指针,每个指针占用一块内存。 |
示例 |
|
|
访问方式 |
|
|
实际作用 | 用于函数参数传递,操作整个数组。 | 用于存储多个指针,可以灵活管理多个对象。 |
6.请问NULL和nullptr区别?
NULL
- 通常,
NULL
被定义为整数值0
。
#define NULL 0
- 所以说NULL实际上是一个空指针,如果在C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
int *pi = NULL; char *pc = NULL;
- 问题:由于
NULL
是整数常量0
,在C++中重载函数中容易导致歧义。可能引发隐式类型转换错误。
void func(int); void func(char*); func(NULL); // 可能调用 func(int),而不是 func(char*)
nullptr
nullptr
是 C++11 引入的新关键字,用于表示空指针。其实本质nullptr
是一种特殊的类型std::nullptr_t
,专门设计用于表示指针类型的空值。最重要的是nullptr
只能用于指针,而不能隐式转换为整数类型。
void func(int); void func(char*); func(nullptr); // 明确调用 func(char*)
- 不会引发因类型不匹配导致的歧义。
- nullptr 可以隐式转换为任何指针类型,但不能转换为整数。
int* p = nullptr; // 正确 int val = nullptr; // 错误:不能将 nullptr 转换为整数
本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中