笔者有幸得到一次和行业前辈交流的机会,前辈也分享了他对于职业规划、人生发展的理解和感悟,笔者也获益颇丰。另外由于笔者主要方向是C/C++,刚入门不久,前辈也是针对能力提升方面推荐了很多课程和书籍,其中就有这本 《高质量程序设计指南C/C++》作者林锐,第三版。
笔者先大致浏览了一遍该书,发现有很多平时开发或学习中没有注意到的小细节,因此新开一帖,作为自用的学习笔记。本系列由于是读书笔记,因此主要会记录平时没有留意的细节问题,并针对这些问题会提出一些额外问题和分析,如底层实现和延伸思考。
自己也是刚入门不久,可能会有很多错误,欢迎大家一起学习,不吝赐教,有任何问题可以评论私信。
上一篇 林锐《高质量程序设计指南》笔记01 ,主要记录了一些关键字和编译器实现的底层原理。
本篇主要集中在书的第六章到第十一章,包括指针、数组等数据结构,以及函数等相关的细节问题。
文章目录
第六章
一、断言(assert)
1、什么是断言:
如果表达式的值为0(假),则输出错误信息,调用abort()终止程序;如果表达式为真,则不进行任何操作。
2、如何使用:
由于assert只能在测试阶段使用,即在Debug版本有效,而在Release版本无效,因此需要在条件编译指令中包含assert的宏体。
# include<assert.h> // assert的头文件
# define DEBUG // Debug测试版本,release 版本注释掉即可
# ifdef DEBUG
# define ASSERT(f) assert(f)
# else
# define ASSERT(f) ((void)0)
# endif
assert(表达式);
3、使用场景:
在函数入口处,使用断言检测参数是否合法
double sqrt_positive(double x) {
assert(x > 0);
... // 代码执行
return 0;
}
这是一个简单的计算正数平方根的函数,如果sqrt_positive()
函数的传入参数小于0 ,则表示参数非法,这种情况会导致程序崩溃,这是要避免的,因此使用断言,如果assert(x > 0);
中的x < 0
,则表达式为假,终止程序。
4、注意:
-
assert();
语句只有在Debug版本中才能使用,而在Release版本中被无视。为了不再Debug版本和Release版本下造成差别,assert();
不应该带来任何副作用,因此assert()不是函数,而是宏。 -
使用
assert();
捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
非法情况如无效的参数、空指针、越界的索引等,这种情况会导致程序崩溃,应当避免,因此使用断言,如果出现这种严重情况立即终止。
错误情况如文件打开失败、分配内存失败等,这种应当使用错误和异常进行处理并在代码中执行异常处理和修复,如果使用断言就直接终止程序了。 -
每个断言只能检测一个条件。因为条件过多,当出现错误时,无法判断是哪个条件出错。
-
不能在断言中放入改变源程序数值的语句,例如
assert(++i==3);
参考:
C++断言(assert)
断言是什么
断言(assert)及其作用
第七章
一、数组的访问方式
参考另外一篇文章:C/C++数组访问方式:*a,*a[0],*(*(a+i)+j)详解
补充:
- 当把
&
用于指针时,就是在提取指针变量的地址。不能在一个指针前面连续使用多个&
,如&&p
,因为&p
已经不是一个变量了,不能把&
单独用于一个非变量的东西。不能对字面常量使用&
来取其地址,因为字面常量保存在符号表中,它们没有地址概念。 - 当把
*
用于指针时,就是在提取指针所指向的变量。因此在将*
用于指针时一定要确保该指针指向一个有效的和合法的变量。不能对void*
类型指针使用*
来取其所指向的变量。
二、字符数组、字符指针和字符串
参考另外一篇文章:sizeof(),strlen(),length(),size()区别
补充:
需要注意的是,在对字符数组进行动态创建、初始化和删除的时候,有没有最后的\0
结束符都不会产生任何影响,因此不能把其作为字符串看待,而应该视为一个数组,如:
char *p = new char[10];
delete []p;
使用delete []p;
进行删除,而不是delete p;
。
参考:
二维数组的首地址、首行地址和元素地址
C语言学习之:一维数组、二维数组的取值和取地址问题
关于二维数组a[i][j]
为什么C语言中*(a+i)+j能表示a[i][j]的地址
第八章
一、位域
位域是一种特殊的结构体成员,它可以用来指定成员占用的位数,从而节省存储空间。位域的定义格式如下:
数据类型 [成员名] : 位数;
例如,如果有一个结构体定义如下:
struct bs {
unsigned int a: 8;
unsigned int b: 2;
unsigned int c: 6;
};
那么这个结构体中,a 成员占用 8 位,b 成员占用 2 位,c 成员占用 6 位,共计 16 位,也就是 2 个字节。如果没有使用位域,这个结构体将占用 12 个字节,因为每个成员都会按照 int 类型的大小来分配空间。
位域的特点和使用方法如下:
- 定义位域时,可以指定成员的位域宽度,即成员所占用的位数。
- 位域的宽度不能超过其数据类型的大小,因为位域必须适应所使用的整数类型。
- 位域的数据类型可以是
int
、unsigned int
、signed int
等整数类型,也可以是枚举类型。 - 位域可以单独使用,也可以与其他成员一起组成结构体。
- 位域的访问是通过点运算符
(.)
来实现的,与普通的结构体成员访问方式相同。
使用场景:
如bool
类型,我们只需要用1
或0
来表示TRUE
或FALSE
,这样就占用1
位即可,但实际上bool
类型在机器中占用1
字节,即8
位。但是通过位域可以节省空间:
struct status
{
unsigned int my_bool : 1;
};
参考:
C语言位域(位段)详解
C 位域
第九章
一、条件编译
1)条件编译指令
需要注意以下几点:
- 每一个条件编译块都必须以
#if
开始,以#endif
结尾,并且一个#if
与它下面的一个#endif
配对。 defined
必须结合#if
或#elif
使用,不能单独使用。- 条件编译可以出现在代码的任何地方。
#ifdef ABC
等价于#if defined(ABC)
,如果前面曾使用#define
定义过ABC
,则#ifdef ABC
表达式为真。#ifndef ABC
等价于#if !defined(ABC)
。
2)条件编译的用法
#ifndef - #define - #endif
#ifndef PI
#define PI 3.1416
#endif
用于判断是否已经定义了名为 PI
的宏,如果没有定义 PI
,则执行#define PI 3.1416
。
如果检测到已经定义了PI
,则不再重复执行上述宏定义。
#if - #elif - #else - #endif
#if 条件表达式1
程序段 1
#elif 条件表达式2
程序段 2
#else
程序段3
#endif
先判断条件1的值,如果为真,则程序段 1 被选中编译;如果为假,而条件表达式 2 的值为真,则程序段 2 被选中编译;其他情况,程序段 3 被选中编译。
#ifdef - #endif
#ifdef N
#undef N
//程序段
#endif
如果检测到符号 N 已定义,则删除其定义,并选中相应的程序段。
3)内部包含卫哨 和 外部包含卫哨
内部包含卫哨 和 外部包含卫哨是两种用来防止头文件被重复包含的预处理器技术。它们的区别在于宏定义的位置和作用范围。
- 内部包含卫哨
内部包含卫哨是在头文件中使用的,它通过定义一个特定的宏来标记该头文件是否已经被包含过,如果已经被包含过,就跳过该头文件的内容,否则就编译该头文件的内容。内部包含卫哨的格式如下:
#ifndef _HEADER_H_ //如果宏_HEADER_H_没有被定义
#define _HEADER_H_ //则定义宏_HEADER_H_
... //头文件的内容
#endif //结束条件编译
- 外部包含卫哨
外部包含卫哨是在源文件中使用的,它通过检测一个特定的宏是否已经被定义来决定是否包含某个头文件,如果已经被定义,就不再包含该头文件,否则就包含该头文件,并且定义该宏。外部包含卫哨的格式如下:
#if !defined (_HEADER_H_) //如果宏_HEADER_H_没有被定义
#include "header.h" //则包含头文件header.h
#define _HEADER_H_ //并且定义宏_HEADER_H_
#endif //结束条件编译
参考:
条件编译,C语言条件编译详解
C++编译预处理—内部包含卫哨和外部包含卫哨
内部包含卫哨和外部包含卫哨
总结
本篇主要集中在介绍C/C++中的指针、数组和字符串,以及数据结构和预处理技术,在下一篇会开始介绍C++面向对象的程序设计方法。
该系列:
林锐《高质量程序设计指南》笔记01