C/C++语言知识点一
目录
1. 请对这段代码进行解释:char *const *(*next)( );
2. 函数指针数组:解释这个表达式char *(*c[10])(int **p);
3. 字符串常量:分析下面这段代码。
4. 访问指定内存地址
5. typedef 和 define 的区别
6. 函数返回局部变量地址问题
7. 无符号整数和有符号整数相加问题
8. 大端模式和小端模式如何用代码判断
9. 显示无符号int类型的最大值和最小值
10. 逻辑运算符 (&&) 和 位运算符 (&)
11. printf函数的返回值
12. 通过#运算符,利用宏参数来创建字符串
13. “##” 运算符的作用
14. 结构体中使用字符数组还是字符指针
15. 内存越界问题
16. free 和 delete 是如何处理指针
1. 请对这段代码进行解释:char *const *(*next)( );
next是一个函数指针,指向一个没有参数的函数,并且该函数的返回值是一个指针,该指针指向一个类型为char的常量指针。
1.(*next)表示next是一个指针,指向某个函数,因为后面跟着(),所以这个指针指向一个函数。
2.next是一个函数指针,函数 (*next)() 的返回类型是 char *const * :
最外层 *:返回值是一个指针
const:该指针是常量(指针本身不可修改)
char *:这个常量指针指向一个 char 类型的指针
3.char *const,这表示一个指向char的指针,而且这个指针是const的,也就是说,它指向的地址不能被修改。
讨论:我认为,char * const *(*next)( )指的是,返回值为char * const *(这是一个char类型的二级指针)的函数指针,我们把p代指这个二级指针,这个p具有的特点为,p的值可以改变,*p的值不能改变,**p的值可以改变,因为const的原理是它后面修饰的变量不能改变,而在p中const后面只有一个*,所以仅*p不能改变其值。
2. 函数指针数组:解释这个表达式char *(*c[10])(int **p);
c
是一个数组,包含 10 个函数指针,每个函数指针指向的函数接受一个 int **
类型的参数,并返回一个 char *
类型的结果。
char *(*c[10])(int **p);
分析:
c 是一个数组,包含10个元素,每个元素是函数指针;
*c 表示数组的每个元素都是指针;
(*c) 后的 (int **p) 表示这些指针指向函数,且函数接受一个 int** (指向整型指针的指针)类型的参数;
最外层的 char * 表示这些函数返回 字符指针(char*)。
螺旋法则(Clockwise/Spiral Rule)详解:
螺旋法则是 解析C语言复杂声明 的核心方法,由计算机科学家 David Anderson 提出。它通过从变量名出发,按顺时针方向(螺旋式)解析修饰符,帮助开发者直观理解多层指针、数组和函数组合的声明。
解析步骤(四步法则):
- 起点:从变量名(标识符)开始
- 方向:按顺时针方向(优先向右,无法继续时向左)解析修饰符
- 优先级:
()
>[]
>*
- 终止:处理完所有修饰符或遇到结束符
;
3. 字符串常量:分析下面这段代码。
char *s = "AAA";
printf("%s", s);
s[0] = 'B';
printf("%s", s);
解析:s[0] = 'B';
这行代码试图修改字符串常量的第一个字符,这是不安全的,因为字符串常量通常存储在只读内存区域。
4. 访问指定内存地址
问题:嵌入式系统经常需要程序员访问特定的内存位置,读取或写入新的值,尤其是在嵌入式处理器开发中操作寄存器时。例如,在某个项目中,需要设置一个绝对内存地址为0x40020800的位置,并将该地址的内容设置为整数值0x3456。现在需要编写代码来完成这个任务。
考点:访问地址时,强制类型转换,强制为一个整形数,该操作是合法 的。
#include <stdint.h> // 包含标准整型定义
// 方法1:直接指针操作
*(volatile uint32_t *)0x40020800 = 0x3456;
// 方法2:宏定义(推荐)
#define REGISTER_ADDR ((volatile uint32_t *)0x40020800)
*REGISTER_ADDR = 0x3456;
// 方法3:通过结构体映射(适用于多寄存器场景)
typedef struct {
volatile uint32_t reg;
} DeviceReg;
DeviceReg *const device = (DeviceReg *)0x40020800;
device->reg = 0x3456;
// 方法4:创建一个指向特定地址的指针
volatile uint32_t *reg = (uint32_t *)0x40020800;
*reg = 0x3456;
注意:
1. 是否需要使用volatile关键字?是的,因为访问硬件寄存器必须使用volatile,防止编译器优化掉写入操作,或者重复读取时使用缓存的值。
2. uint32_t
和 unsigned int
的主要区别在于大小的固定性。uint32_t
始终是32位,而 unsigned int
的大小可能会因平台而异。在跨平台编程、嵌入式系统或处理网络协议和文件格式时,使用 uint32_t
可以确保数据类型的精确匹配,避免溢出或截断问题。
5. typedef 和 define 的区别
5.1 功能与行为
(1)#define 是预处理指令,在编译前进行文本替换。
#define INT_PTR int *
INT_PTR a, b; // 展开为 int *a, b; (b 是 int 类型,非指针)
(2)typedef 是类型别名定义,由编译器处理,创建新的类型名称。
typedef int *INT_PTR;
INT_PTR a, b; // a 和 b 都是 int* 类型
5.2 作用域
(1)#define 从定义点开始,到文件末尾或 #undef 取消定义为止。无块作用域,不可在局部范围内定义。
void func() {
#define LOCAL_MACRO 10 // 宏定义作用域为整个文件
}
(2)typedef
支持块作用域,可在局部范围内定义。
void func() {
typedef int MyInt; // 类型别名作用域为 func 函数内
MyInt a = 10;
}
5.3 类型检查
#define
无类型检查,直接替换文本,可能导致意外错误。
typedef
由编译器处理,支持类型检查,语义更明确。
5.4 复杂类型支持
#define
需要额外括号和小心使用,否则可能出错。
#define FUNC_PTR int (*)(int)
FUNC_PTR f1, f2; // 展开为 int (*f1)(int), f2; (f2 是 int 类型,非函数指针)
typedef
直接支持复杂类型,语法更清晰。
typedef int (*FUNC_PTR)(int);
FUNC_PTR f1, f2; // f1 和 f2 都是函数指针类型
5.5 与 const
结合
#define
宏替换可能导致意外结果。
#define PTR int *
const PTR a = NULL; // 展开为 const int *a = NULL; (a 是指向常量的指针,非常量指针)
typedef
语义明确,与 const
结合行为可预测。
typedef int *PTR;
const PTR a = NULL; // a 是常量指针,类型为 int *const
5.6 使用场景对比
适合使用 #define 的场景:
// 1.定义常量:
#define PI 3.14159
// 2.条件编译:
#define DEBUG
#ifdef DEBUG
printf("Debug mode\n");
#endif
// 3.简单文本替换:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
适合使用 typedef 的场景:
// 1.定义类型别名:
typedef unsigned char uint8_t;
// 2.简化复杂类型:
typedef int (*FuncPtr)(int, int);
// 3.提高代码可读性:
typedef struct {
int x;
int y;
} Point;
6. 函数返回局部变量地址问题
问题:请指出下面这段代码的错误。
#include <stdio.h>
char *get_str(void);
int main(void)
{
char *p = get_str();
printf("%s\n", p);
return 0;
}
char *get_str(void)
{
char str[] = {"abcd"};
return str;
}
问题分析
-
局部变量的生命周期
-
get_str
函数中,str
是一个 局部数组,其内存分配在栈上。 -
当
get_str
函数返回时,str
的内存会被释放,返回的指针指向 无效内存。
-
-
未定义行为
-
在
main
函数中,p
指向了get_str
返回的地址,但此时该地址的内容已经无效。 -
访问
p
会导致 未定义行为(程序可能崩溃、输出乱码或看似正常但存在隐患)。
-
如何修改:
方案1:返回静态局部变量(不推荐)
将 str
声明为 static
,使其生命周期延长到程序结束:
char *get_str(void)
{
static char str[] = {"abcd"}; // 静态局部变量
return str;
}
- 优点:简单直接,程序可以正确运行。
- 缺点:静态局部变量在程序运行期间始终占用内存,且多线程环境下不安全。
方案2:返回字符串常量(推荐)
直接返回字符串常量,常量存储在只读内存中,生命周期为整个程序:
char *get_str(void)
{
return "abcd"; // 字符串常量
}
- 优点:简单高效,内存安全。
- 缺点:返回的字符串不可修改(只读)。
方案3:返回字符串常量的地址
#include <stdio.h>
char *get_str(void);
int main(void)
{
char *p = get_str();
printf("%s\n", p); // 输出 "abcd"
return 0;
}
char *get_str(void)
{
char *str = {"abcd"}; // 指针指向字符串常量
return str;
}
- 优点:程序可以正确运行,不会出现未定义行为。
- 缺点:返回的字符串不可修改(只读)。但是,这种用法仍然存在问题,因为它违反了函数的封装性,即函数的实现细节(如返回字符串常量的指针)应该对调用者隐藏。
方案4:动态分配内存(推荐)
使用 malloc
动态分配内存,返回堆上的地址:
#include <stdio.h>
#include <stdlib.h> // 引入stdlib.h以使用malloc和free
char *get_str(void);
int main(void)
{
char *p = get_str();
if (p != NULL) {
printf("%s\n", p);
free(p); // 释放内存
}
return 0;
}
char *get_str(void)
{
char *str = (char *)malloc(5 * sizeof(char)); // 动态分配内存
if (str == NULL) {
return NULL; // 如果内存分配失败,返回NULL
}
strcpy(str, "abcd"); // 复制字符串到动态分配的内存
return str;
}
- 优点:返回的字符串可修改,内存管理灵活。
- 缺点:需要手动释放内存,否则会导致内存泄漏。
7. 无符号整数和有符号整数相加问题
段代码的目的是检查两个整数相加的结果是否大于6,并根据结果输出相应的字符串。
#include <stdio.h>
int main(void)
{
unsigned int a = 6; // 无符号整数
int b = -20; // 有符号整数
(a + b > 6) ? puts(">6") : puts("<=6");
return 0;
}
注意:
1.在 C 语言中,当无符号整数和有符号整数混合运算时,有符号整数会 隐式转换 为无符号整数。
2.在计算机中,整数通常以补码的形式存储。
3.负数的补码
a + b
的实际计算过程:a
是无符号整数,值为6
。b
被转换为无符号整数,其值为UINT_MAX - 19
(假设unsigned int
是 32 位,则值为4294967276
)。- 因此,
a + b
的值为6 + 4294967276 = 4294967282
。
8. 大端模式和小端模式如何用代码判断
- 大端模式:数据的 高位字节 存储在内存的 低地址 处。
- 小端模式:数据的 低位字节 存储在内存的 低地址 处。
方法1:通过检查一个多字节数据(如 int
)在内存中的存储方式,可以判断当前系统的字节序。
#include <stdio.h>
int is_little_endian() {
int num = 1; // 定义一个整数
char *p = (char *)# // 将整数的地址转换为 char*
return *p == 1; // 检查第一个字节是否为 1
}
int main() {
if (is_little_endian()) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
方法二:使用联合体判断。联合体的特性是 所有成员共享同一块内存,因此可以通过联合体访问同一数据的不同字节。
#include <stdio.h>
int is_little_endian() {
union {
int num; // 多字节类型
char c; // 单字节类型
} u;
u.num = 1; // 将联合体的 int 成员设为 1,其二进制表示为 0x00000001
return u.c == 1; // 检查 char 成员是否为 1
}
int main() {
if (is_little_endian()) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
9. 显示无符号int类型的最大值和最小值
为了正确地获取当前系统下 unsigned int
类型的最大值,可以使用 UINT_MAX
宏,这个宏定义在 <limits.h>
或 limits
头文件中:
#include <stdio.h>
#include <limits.h>
int main() {
unsigned int zero = 0;
unsigned int max_value = UINT_MAX;
printf("The value of 0 for unsigned int: %u\n", zero);
printf("The maximum value for unsigned int: %u\n", max_value);
return 0;
}
注意:也可以直接对0取反操作。
unsigned int zero = 0;
unsigned int max_value = ~zero; // 对0取反,得到无符号整数的最大值
10. 逻辑运算符 (&&
) 和 位运算符 (&
)
关键区别:
- 短路求值:
&&
会短路,避免不必要的计算和潜在的错误。&
不会短路,总是计算所有条件。
- 安全性:
- 使用
&&
是安全的,因为它可以防止数组越界访问。 - 使用
&
是危险的,可能会导致程序崩溃或未定义行为。
- 使用
11. printf函数的返回值
代码分析:答案:4321
#include <stdio.h>
int main() {
int i = 43;
printf("%d\n", printf("%d", printf("%d", i)));
return 0;
}
总结:
printf
的返回值是打印的字符数。- 嵌套的
printf
调用会从内到外依次执行。 - 最终的输出是
4321
,包括换行符。
12. 通过#运算符,利用宏参数来创建字符串
在 C 语言中,#
运算符是 字符串化运算符,它可以将宏参数转换为字符串字面量。通过结合宏定义和 #
运算符,可以轻松地将宏参数转换为字符串。
代码分析:
#include <stdio.h>
// 定义宏
#define SQUARE(x) (printf(""#x" square is: %d\n", (x) * (x)))
int main() {
int num = 5;
SQUARE(num);
SQUARE(3 + 2);
SQUARE(10);
return 0;
}
输出:
num square is: 25
3 + 2 square is: 25
10 square is: 100
解释:
-
SQUARE(num)
:#x
被替换为"num"
。(x) * (x)
计算为5 * 5 = 25
。- 输出:num square is: 25
-
SQUARE(3 + 2)
:#x
被替换为"3 + 2"
。(x) * (x)
计算为5 * 5 = 25
。- 输出:3 + 2 square is: 25
-
SQUARE(10)
:#x
被替换为"10"
。(x) * (x)
计算为10 * 10 = 100
。- 输出:10 square is: 100
总结:
- 这个宏定义通过
#
运算符将参数x
转换为字符串,方便在输出中显示参数的具体形式。 - 同时,宏会计算参数的平方值并输出。
- 这种技巧在调试或需要显示表达式及其结果时非常有用。
13. “##” 运算符的作用
在 C 语言中,##
是 令牌粘贴运算符(Token Pasting Operator),用于将两个令牌(tokens)连接成一个新的令牌。它在宏定义中非常有用,特别是在需要动态生成标识符或代码时。
基本用法:
#include <stdio.h>
// 定义宏,将两个令牌连接
#define CONCAT(a, b) a##b
int main() {
int var1 = 10;
int var2 = 20;
// 使用宏连接令牌
printf("%d\n", CONCAT(var, 1)); // 输出 var1 的值
printf("%d\n", CONCAT(var, 2)); // 输出 var2 的值
return 0;
}
进阶用法:动态生成代码。##
运算符可以用于动态生成变量名、函数名或代码片段。
#include <stdio.h>
// 定义宏,动态生成函数名
#define CALL_FUNCTION(func_name, arg) func_name##_version_##arg()
// 定义函数
void greet_version_1() {
printf("Hello from version 1!\n");
}
void greet_version_2() {
printf("Hello from version 2!\n");
}
int main() {
// 使用宏调用不同版本的函数
CALL_FUNCTION(greet, 1); // 调用 greet_version_1
CALL_FUNCTION(greet, 2); // 调用 greet_version_2
return 0;
}
14. 结构体中使用字符数组还是字符指针
代码分析:
#include <stdio.h>
#include <string.h>
struct std {
unsigned int id;
char *name;
unsigned int age;
} per;
int main(void) {
per.id = 0001;
strcpy(per.name, "Micheal Jackson");
per.age = 20;
printf("%s\n", per.name);
return 0;
}
结论:
- 优先使用字符数组。
- 如果写成 per.name = "Micheal Jackson" ,只是将字符串的地址给了指针,该字符串的内容并不在结构体内,无法直接修改。
15. 内存越界问题
分析代码:
char *p1 = "ABCABC";
char *p2 = (char *)malloc(strlen(p1));
strcpy(p2, p1);
分析:
-
strlen
函数:这个函数计算字符串的长度,但是不包括字符串结束符'\0'
。因此,对于字符串"ABCABC"
,strlen(p1)
的结果是 6,而不是 7(如果包括'\0'
)。 -
strcpy
函数:这个函数用于复制字符串,并且会连同字符串结束符'\0'
一起复制。
16. free 和 delete 是如何处理指针
#include <iostream>
#include <cstring>
using namespace std;
int main(void)
{
char *p = new char[100]; // 使用new操作符分配100字节的内存空间,用于存储字符数组
strcpy(p, "ABCABC"); // 使用strcpy函数将字符串"ABCABC"复制到p指向的内存空间
cout << "p = " << p << endl; // 输出p指向的字符串
delete [] p; // 使用delete[]操作符释放p指向的内存空间
p = NULL; // 将p设置为NULL,避免悬垂指针问题
return 0;
}
结论:两者只是将指针指向的内存给释放掉了,但是指针还是存在;不能再次访问的原因是此时指针是野指针,可能出现不必要的麻烦
改进操作:
-
释放后置空指针
在调用 free 或 delete 后,将指针置为 NULL(C)或 nullptr(C++),避免悬空指针。 free(ptr); ptr = NULL; delete ptr; ptr = nullptr;
-
避免重复释放:确保每个指针只释放一次。
-
匹配分配和释放函数:
- 使用
malloc
分配的内存用free
释放。 - 使用
new
分配的内存用delete
释放。 - 使用
new[]
分配的内存用delete[]
释放。
- 使用
-
检查空指针:在释放前检查指针是否为空,避免不必要的操作。
总结:
free
和delete
用于释放动态分配的内存,但不会自动置空指针。- 释放后,指针仍然指向原来的地址,但访问它是未定义行为。
- 最佳实践是在释放后将指针置空,并避免重复释放。