18_C指针的使用规范、位运算
指针的使用规范
在C和C++中,指针是强大的工具,但如果不小心使用,可能会引发复杂的错误。因此,了解指针的规范使用原则可以帮助编写更安全和易于维护的代码。以下是一些使用指针的规范:
1. 初始化指针
指针变量在声明后,应尽量初始化,避免使用未初始化的指针导致未定义行为。常见的初始化方式包括:
将指针设置为
nullptr
(C++11起)或NULL
(C语言中):
int* ptr = nullptr; // C++
int* ptr = NULL; // C
2. 检查指针的有效性
在解引用指针前,必须检查指针是否为空,确保它指向合法的内存地址,以防止空指针异常。
if (ptr != nullptr) {
// 安全地使用指针
}
3. 使用指针前进行内存分配
在动态内存分配时(如malloc
或new
),要确保分配成功后再使用指针。
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
// 分配失败处理
}
4. 避免野指针
释放指针所指向的内存后,将指针设置为nullptr
或NULL
,避免指针成为“野指针”:
free(ptr);
ptr = NULL;
5. 不要重复释放指针
重复释放会导致崩溃或不可预测的行为。确保在释放指针之前检查其是否为nullptr
。
6. 注意指针运算
指针运算可能导致访问非法地址。尤其在数组、字符串等场景中小心进行指针偏移操作,避免越界访问。
7. 合理使用智能指针(C++)
在C++中,使用智能指针(如std::unique_ptr
和std::shared_ptr
)可以减少手动管理内存的负担,防止内存泄漏:
std::unique_ptr<int> ptr = std::make_unique<int>(10);
8. 避免指针混用
当同一块内存区域有多个指针指向时,容易产生悬挂指针和未定义行为,建议尽量减少多指针对同一内存的直接操作,特别是在多线程场景下。
9. 使用const修饰符
当指针所指的值不应被修改时,使用const
修饰指针或指针指向的对象,提升代码的安全性和可读性。
const int* ptr; // 指针指向的数据不可修改
int* const ptr; // 指针本身不可修改
const int* const ptr; // 指针和数据都不可修改
10. 注意释放动态数组内存
在使用new[]
分配动态数组时,应使用delete[]
来释放内存,以避免未定义行为。
int* arr = new int[10];
delete[] arr;
11. 避免返回局部变量的地址
在函数中返回局部变量的地址是一个常见错误,因为函数结束后,局部变量会被销毁。
int* func() {
int x = 10;
return &x; // 错误
}
按照这些规范,能够更安全地管理指针的生命周期和访问权限,从而避免许多常见的指针错误。
多资源控制问题
在多资源控制问题中,尤其是多线程或多进程环境中,合理管理和控制资源以避免竞争、死锁等问题是关键。以下是一些多资源控制的规范和建议:
1. 避免死锁(Deadlock)
死锁是多个资源竞争时常见的问题。避免死锁可以通过以下方法实现:
固定资源请求顺序:所有线程按照相同的顺序申请资源。比如,始终先申请资源A再申请资源B。
使用超时机制:设置资源获取的超时,当等待超时则释放已持有的资源。
死锁检测:通过算法定期检测死锁情况,并根据情况释放部分资源。
2. 使用互斥锁(Mutex)保护共享资源
在多个线程访问同一共享资源时使用互斥锁,避免数据竞争问题。
使用语言提供的互斥锁,如C++的
std::mutex
或Python的threading.Lock
。尽量缩小锁的范围,仅锁定访问共享资源的代码段,减少锁的持有时间,提高效率。
3. 采用读写锁(Read-Write Lock)
在读多写少的场景中,使用读写锁提高资源访问效率。读写锁允许多个读线程并发执行,但写线程必须独占。
C++中可以使用
std::shared_mutex
和std::unique_lock
控制读写锁。避免多个写操作同时进行,以防止竞争和资源不一致。
4. 使用条件变量(Condition Variable)实现同步
条件变量是协调线程执行的常用工具,用于等待特定条件成立时才开始执行。
在C++中使用
std::condition_variable
。在Python中可以使用
threading.Condition
。确保条件变量的状态在适当条件下更新,避免线程长时间等待或进入无用的忙等待状态。
5. 资源池管理
对于多线程使用频繁的资源(如数据库连接、线程、文件句柄等),可以采用资源池模式,减少创建和销毁资源的开销。
在C++中可以通过对象池(如使用
std::vector
管理一组资源)来实现。在Python中可以使用
multiprocessing.Pool
管理多进程任务。注意资源池大小设置,防止资源不足或资源闲置。
6. 使用原子操作
对于简单计数、标志位等轻量资源的操作,优先使用原子操作,以减少锁的使用,提升性能。
C++中可以使用
std::atomic
操作整型变量。Python中多线程环境下可以使用
queue
或multiprocessing.Value
控制共享状态。原子操作适用于轻量、短时间占用的资源,复杂资源共享应配合互斥锁使用。
7. 避免不必要的资源共享
尽量减少资源共享的范围,尤其是跨线程共享,限制资源的使用范围,提高系统的可维护性和安全性。
使用局部变量来避免不必要的共享。
仅将资源共享给需要的线程或模块。
8. 选择合适的锁类型
不同场景中可以根据需求选择适合的锁类型:
递归锁(Recursive Lock):允许同一线程多次申请相同的锁。适用于递归调用等情形。
自旋锁(Spin Lock):适用于锁时间很短的情况,避免线程切换的开销,但不适合长时间锁定资源。
互斥锁(Mutex):常用于保护较长时间的资源操作,确保线程独占访问。
9. 定期释放资源
长时间占用资源会导致资源紧张,尤其是内存、文件句柄等,应尽量释放不再使用的资源。
采用RAII(Resource Acquisition Is Initialization)模式,通过对象的生命周期管理资源。
定期清理资源池中的闲置资源。
10. 检查并避免活锁(Livelock)
活锁是线程或进程不断改变状态却始终无法达成目标的一种情况。避免活锁的办法包括:
增加随机等待时间,减少线程间频繁相互让出资源的现象。
设置一个循环检查计数器,当超出限制时强制解锁或释放资源。
通过以上多资源控制的规范和最佳实践,能够大大减少资源竞争、死锁、活锁等问题,提高程序的效率和稳定性。
#include <stdio.h>
#include <string.h>
int main (int argc, char **argv, char *envp[]) {
void ErrorProc()
{
}
int *pA = (int *)malloc(sizeof(int));
if(pA == NULL)
{
ErrorProc();
return -1;
}
*pA = 999;
char *pB1;
float *pB2;
if (argc % 2 == 0)
{
pB1 = (char *)malloc(strlen("Hello") + sizeof(char));
if(pB1 == NULL)
{
free(pA);
ErrorProc();
return -1;
}
strcpy(pB1, "Hello");
}
else
{
pB2 = (float *)malloc(sizeof(float));
if(pB2 == NULL)
{
free(pA);
ErrorProc();
return -1;
}
*pB2 = 3.14f;
}
double *pC = (double *)malloc(sizeof(double));
if (pC == NULL)
{
free(pB2);
free(pB1);
free(pA);
ErrorProc();
return -1;
}
*pC = 0.618;
}
在多资源管理中,堆中我们申请了A,然后分支下去B1,B2,然后又指向C。是一个菱形。这种时候我们如果发生错误,我们应该从后向前挨个释放。
#include <stdio.h>
#include <string.h>
#define SAFE_FREE(p) if(p){free(p);(p)=NULL;}
void ErrorProc()
{
}
int main (int argc, char **argv, char *envp[])
{
//多资源使用规范
//1.引用资源的变量在作用域开始定义, 并初始化为错误值。
int *pA = NULL;
int *pB1 = NULL;
int *pB2 = NULL;
double *pC = NULL;
//2.申请资源后,必须检查是否资源有效,无效则处理错误
pA = (int *)malloc(sizeof(int));
if (pA ==NULL)
{
ErrorProc();
//3.处理完错误后,跳转到统一退出位置
goto EXIT_PROC;
}
//资源有效正常使用
*pA = 999;
EXIT_PROC:
SAFE_FREE(pC);
SAFE_FREE(pB2);
SAFE_FREE(pB1);
SAFE_FREE(pA);
/*
//4.释放资源前,必须要检查资源是否有效,无效则不处理
if (pA != NULL)
{
free(pA);
//5.释放资源后,必须将引用资源的变量重置为错误值
pA = NULL;
}
*/
return 0;
这个结构就是标准的多资源流程,在cpp里有try catch,goto有throw
位运算
A and 1 = A
A and 0 = 0
A or 1 = 1
A xor A = 0
A xor 0 = A
A xor 1 = $\neg$A
A and $\neg$ A= 0
A or $\neg$A =
#include <stdio.h>
int my(int n )
{
unsigned int i = 7;
i = i >> 1; //3
i = -1;
i = i >> 1; //0xffffffff >> 1 = 0x7fffffff
int j = 7;
j = j >> 1; //3
i = -1;
i = i >> 1; // 0xffffffff >> 1 = oxffffffff
// f(0x1111) -> 7(0x111) -> 3(0x11) -> 1(0x1) -> -1(-0x1) -> -1(0x1)
// 移位前面补充0,如果是-1前面补符号位,本身不变
}
int myabs(int n)
{
int i = n >> 31; // if n >= 0, i = 0; else i = 0xffffffff = -1
n = n ^ i; // if i = 0, n = n; else i = 0xffffffff, n = ~n
return n - i; // if i = 0, n - i = n; else i = -1, n - i = n + 1
}
int main ()
{
int n = myabs(-5);
return 0;
}
myabs2
函数
这个函数通过位运算实现绝对值的计算,具体步骤如下:
int i = n >> 31;
这一步利用右移运算符获取
n
的符号位。若n
是负数,i
会被赋值为0xFFFFFFFF
(即 -1);若n
是正数或零,i
将为 0。n = n ^ i;
使用异或运算来翻转
n
的所有位。当i
为 0 时,n
不变;当i
为 -1 时,n
的所有位都被翻转,即n
变成~n
(取反)。return n - i;
如果
i
是 0,则返回n
;如果i
是 -1,则返回n + 1
。这一步的目的是处理负数情况下的补偿。
main
函数
int fuck = 0;
:定义一个整数变量。scanf("%d", &fuck);
:读取用户输入的整数。int n = myabs2(fuck);
:调用myabs2
函数,计算绝对值并赋值给n
。printf("%d", n);
:输出绝对值。
总结
myabs2
函数以高效且简洁的方式计算了整数的绝对值,利用了位运算和逻辑运算。myabs
函数则没有实现有效的功能,可能是一个未完成或错误的示例。总体来说,代码展示了如何使用位运算来实现简单的数学计算。
算数移动
左移和右移是位运算中的基本操作,以下是它们的规则和补位方式:
左移(<<)
操作:将二进制数的所有位向左移动指定的位数。
规则:
移动后,低位(右侧)用 0 填充。
左移相当于乘以 2 的 n 次方(n 为移动的位数)。
示例:
0000 0001
(1)左移 1 位:
0000 0001 << 1 --> 0000 0010 (2)
右移(>>)
操作:将二进制数的所有位向右移动指定的位数。
规则:
算术右移(通常用于带符号整数):最高位(符号位)保持不变,填充新位为符号位的值(1 或 0)。
如果是负数,补位为 1;如果是非负数,补位为 0。
逻辑右移:不考虑符号位,始终用 0 填充新位(通常用于无符号整数)。
示例:
算术右移:
对于
1111 1010
(-6,补码形式)右移 1 位:
1111 1010 >> 1 --> 1111 1101 (-3)
逻辑右移:
对于
1111 1010
(-6,补码形式)逻辑右移 1 位:
1111 1010 >> 1 --> 0111 1101 (125)
总结
左移:将数的所有位向左移动,用 0 填充低位。相当于乘以 2 的 n 次方。
右移:
算术右移:保持符号位,符号位填充新位。
逻辑右移:无论符号,均用 0 填充新位。
不同的编程语言可能对这两种右移的实现有所不同,所以在使用时要注意具体的语言规则。
权限控制
#define ADD 1 //0001
#define DEL 2 //0010
#define EDT 4 //0100
#define QUE 8 //1000
#include <cstdio>
int main ()
{
int nPrivilege = 0;
nPrivilege = nPrivilege | DEL;
nPrivilege = nPrivilege | EDT;
if (nPrivilege & ADD)
{
puts("Add Privileges");
}
if (nPrivilege & DEL)
{
puts("Delete Privileges");
}
if (nPrivilege & EDT)
{
puts("Edit Privileges");
}
if (nPrivilege & QUE)
{
puts("Queue Privileges");
}
return 0;
}
int nPrivilege = 0;
nPrivilege = nPrivilege | DEL;
nPrivilege = nPrivilege | EDT;
在这一部分代码中,nPrivilege
先被初始化为 0
,然后通过按位或操作依次添加 DEL
和 EDT
权限。由于 DEL
和 EDT
的值分别是 2
(0010
)和 4
(0100
),执行完这两次赋值操作后,nPrivilege
的值为 6
(二进制表示为 0110
)。
接下来的条件判断逐一检查 nPrivilege
中是否包含 ADD
、DEL
、EDT
和 QUE
权限:
if (nPrivilege & ADD)
:不满足,因为nPrivilege
中没有ADD
权限。if (nPrivilege & DEL)
:满足,nPrivilege
中包含DEL
权限。if (nPrivilege & EDT)
:满足,nPrivilege
中包含EDT
权限。if (nPrivilege & QUE)
:不满足,因为nPrivilege
中没有QUE
权限。
因此,程序的最终输出为:
Delete Privileges
Edit Privileges
这表示当前权限 nPrivilege
中包含 DEL
(删除)和 EDT
(编辑)权限。
位或与位与
位或和位与是两种常见的位运算操作,主要用于处理二进制位。它们的作用分别是设置位(位或)和检测位(位与)。我们分别来看一下它们的特点和用法。
位或(|
)
位或操作会将两个数的每个位进行比较,只要其中一个数的该位是 1
,结果的对应位就是 1
。位或主要用于设置位,即将特定的位设为 1
,通常用于添加权限标志或组合多个标志。
示例
例如,将权限 ADD
(0001
)和 DEL
(0010
)组合到一起:
int nPrivilege = 0;
nPrivilege = nPrivilege | ADD; // 设置 ADD 权限
nPrivilege = nPrivilege | DEL; // 设置 DEL 权限
位或运算过程:
0001 (ADD)
| 0010 (DEL)
----
0011 (结果)
结果为 0011
,表示当前权限包含 ADD
和 DEL
。
位与(&
)
位与操作会将两个数的每个位进行比较,只有两个数的该位都是 1
时,结果的对应位才是 1
。位与主要用于检测位,即检查某个位是否为 1
,常用于判断是否包含某个权限。
示例
假设 nPrivilege
的值为 0011
(表示包含 ADD
和 DEL
),我们可以用位与操作检查它是否包含 DEL
权限。
if (nPrivilege & DEL)
{
puts("Delete Privileges");
}
位与运算过程:
0011 (nPrivilege)
& 0010 (DEL)
----
0010 (结果)
结果为 0010
(非零),说明 nPrivilege
中包含 DEL
权限。
位或和位与的综合应用
在权限控制中,通常通过位或设置权限,通过位与检测权限。例如:
总结
位或 (
|
):用于设置位,将特定的权限添加到nPrivilege
中。位与 (
&
):用于检测位,检查nPrivilege
是否包含特定的权限。
//描述位数关系,注释标准
//nnnnxxxxxxxxxxxxxxxxxxxxrrrr qeda
//a : add
//d : del
//e : edt
//q : que
//n : no use 未使用
//x : no define 未定义
//r : reserved 保留
#include <cstdio>
struct tagPrivilege
{
//按位对应
//31 3210
//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
int add:1; //:1只影响一位
int del:1;
int edt:1;
int que:1;
};
int main ()
{
struct tagPrivilege pri = {0};
pri.del = 1;
pri.edt = 1;
if (pri.add)
{
puts("add");
}
if (pri.del)
{
puts("del");
}
if (pri.edt)
{
puts("edt");
}
if (pri.que)
{
puts("que");
}
}
这段代码定义了一个 tagPrivilege
结构体,通过位域(bit-field)来精确控制每个权限标志的占用位数。具体来说,每个权限标志 (add
、del
、edt
、que
) 都使用一个比特位(int :1
)来表示是否拥有该权限,这样可以节省空间,并且易于按位访问。
代码详解与注释
#include <cstdio>
struct tagPrivilege
{
// 按位对应
// 31 3210
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
int add:1; // :1 表示该字段占用 1 位,代表 "add" 权限
int del:1; // :1 表示该字段占用 1 位,代表 "del" 权限
int edt:1; // :1 表示该字段占用 1 位,代表 "edt" 权限
int que:1; // :1 表示该字段占用 1 位,代表 "que" 权限
};
int main ()
{
struct tagPrivilege pri = {0}; // 初始化所有位为 0(没有权限)
pri.del = 1; // 设置 "del" 权限
pri.edt = 1; // 设置 "edt" 权限
// 检查各权限位并输出对应的权限
if (pri.add)
{
puts("add");
}
if (pri.del)
{
puts("del");
}
if (pri.edt)
{
puts("edt");
}
if (pri.que)
{
puts("que");
}
}
代码逻辑
struct tagPrivilege
结构体中每个字段都使用int :1
,这表示每个字段只占用一个比特位。pri.del = 1;
和pri.edt = 1;
设置了del
和edt
权限位,其余权限保持为0
。在
main
函数中,通过条件判断每个权限位是否为1
,输出对应的权限。
程序的输出
由于设置了 del
和 edt
权限,程序的输出将为:
del
edt
位定义说明
根据注释中的格式定义,可以对各字段的用途进行解释:
a : add
-add
表示添加权限,占用 1 位。d : del
-del
表示删除权限,占用 1 位。e : edt
-edt
表示编辑权限,占用 1 位。q : que
-que
表示查询权限,占用 1 位。n : no use
- 表示未使用的位。x : no define
- 表示未定义的位。r : reserved
- 表示保留位。
每个权限使用一位,按位组合后,能够节省内存并便于权限判断。