《 C和指针》阅读笔记

C和指针

  1. C语言不执行数组下标检查的原因
  1. 不检查下标是否越界可以有效提高程序运行的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以为了程序的运行效率,C/C++才不检查下标是否越界。

  2. 不检查下标是为了给程序员更大的空间,也为指针操作带来更多的方便。如果有这个检查的话指针的功能将会大大被削弱,C的数组标识符,里面并没有包含该数组长度的信息,只包含地址信息,所以语言本身无法检查,只能通过编译器检查,而早期的C语言编译器也不对数组越界进行检查,只能由程序员自己检查确保。以及在早期的CRT函数中也不对字符串指针或数组进行越界检查,都是要求程序员确保空间足够,因此也才也才有了在VS2005之后微软提供的安全的CRT函数版本。

  1. gets与getchar函数

Never use gets()!!!

原型: char * gets ( char * str );

Get string from stdin

Reads characters from the standard input (stdin) and stores them as a C string into str until a newline character or the end-of-file is reached.

  • 参数

指向内存块的指针

  • 返回值

成功时,返回的是str指针,失败时,返回的是空指针。

注意:

gets遇到换行符就停止读入并返回,第一个输入为换行符,字符串置为空串。

gets函数读入再丢弃换行符,换行符将不会存储在字符串

getchar()函数

原型为:int getchar ( void );

Returns the next character from the standard input (stdin).

On success, the character read is returned (promoted to an int value).
The return type is int to accommodate for the special value EOF, which indicates failure:

注:EOF需要的位数比字符型值提供的位数要多,这是getchar返回一个整型值而不是字符值的原因。

  1. 翻译

程序的每个源代码通过编译转为目标代码(.obj),然后各个目标代码由 链接器捆绑在一起,形成一个单一完整的可执行程序(.exe)。

编译由预处理器处理解析两部分组成,有时可能会加入优化选项。

  1. 转义字符
  • 一般转义字符

形式上两个字符组成,但只代表一个字符。如:

\a  \n  \t  \v  \b  \r  \f  \\  \'  \"
  • 八进制转义字符

由反斜杠’\'和随后的1~3个八进制数字构成的字符序列。

'\60' //'0'
'\101'//'A'
'\141'//'a'
  • 十六进制转义字符

是由反斜杠’\'和字母x(或X)及随后的1~2个十六进制数字构成的字符序列。

注意:

  1. 如果反斜线之后的字符和它不构成转义字符,则’\’不起转义作用将被忽略

printf(“a\Nbc\nDEF\n”);
//输出为:
//aNbc
//DEF
  1. 转义字符也可以出现在字符串中,但只作为一个字符看待

\026[12,m长度为6,而\0mn长度为0。

  1. 数据类型

image-20201001201411383

注:长整型至少应该和整型一样长,而整型至少应该与短整型一样长。

  1. typedef

应该使用typedef而不是#define去创建新的数据类型,因为后者无法正确处理指针类型。

#define d_ptr_to_char char*
d_ptr_to_char a, b;//a为指向char的指针 b是字符变量

typdef char *d_ptr_to_char;
d_ptr_to_char a, b;//a和b都为指向char的指针
  1. 常量

常量的值不能被修改。

如:声明整数常量a,可以用int const a或者const int a

涉及指针时,const出现的位置有三个:

const int *pi;//指向整型常量的指针(指向的变量无法修改)
int const *pi;//指向整型常量的指针
int * const pi;//指向整形的常量指针(指针的值无法修改)

注:#defineconst都能声明名字常量,如

#define 	MAX_ELEMENTS 50
const max_elements = 50;

此时#define优于const,在能使用字面值常量的地方均能使用前者,但const常量只能使用在允许使用变量的地方。

用名字常量来定义数组的长度提高了程序的可维护性

  1. 作用域、链接属性和存储类型
变量类型声明的位置是否存于堆栈作用域如果声明为static
全局所有代码块之外从声明处到文件尾不允许从其他源文件访问
局部代码块起始位置整个代码块变量不存储在堆栈,它的值在程序整个执行期一直保持
形式参数函数头部整个函数不允许
  1. switch语句

参考博客.

You can use the break statement to end processing of a particular case within the switch statement and to branch to the end of the switch statement. Without break, the program continues to the next case, executing the statements until a break or the end of the statement is reached. In some situations, this continuation may be desirable

case后没有break时,执行完当前case,会继续执行下一个case,直到达到switch末尾。

所以,要养成每一个case后,加break的习惯

  1. 操作符
  • 指定的位设置为1:value = value | 1 << bit_num

  • 指定的位设置为0:value = value & ~(2 << bit_num)

  • 奇数判断(偶数同理):if((value & 1) == 1)

  • 逗号操作符将两个或多个表达式分隔开,表达式从左向右逐个求值,整个表达式的值 就是最后一个表达式的值。

  • 操作符的优先级决定了相邻的操作符哪个先被执行,优先级相等,结合性将决定执行顺序。但编译器在不违背优先级和结合性的情况下,可以决定求值顺序。

  1. 指针
  • 对所有的指针变量要显式初始化,可以节省大量的调试时间。

  • 指针加(减)一个整型值,是原值乘以指针目标类型的长度,若指针指向同一数组中的元素,可以相减,结果表示指针在数组中的距离。

  1. 可变参数列表

实现步骤:

  • 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
  • 在函数定义中创建一个 va_list 类型变量,该类型是在 stdarg.h 头文件中定义的。
  • 使用 int 参数和 va_start 宏来初始化 va_list 变量为一个参数列表。宏 va_start 是在 stdarg.h 头文件中定义的。
  • 使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。
  • 使用宏 va_end 来清理赋予 va_list 变量的内存。

eg:求平均数

#include <stdio.h>
#include <stdarg.h>
 
double average(int num,...)
{
 
    va_list valist;
    double sum = 0.0;
    int i;
 
    /* 为 num 个参数初始化 valist */
    va_start(valist, num);
 
    /* 访问所有赋给 valist 的参数 */
    for (i = 0; i < num; i++)
    {
       sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);
 
    return sum/num;
}
 
int main()
{
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}
  1. 数组
  • 数组名是指针常量,不能被修改赋值!

  • 除了优先级外,下标访问和间接访问是完全相同的,即arr[sub]等同于*(arr + sub)。另外,在C中sub[arr]也是合法的,它等同于*(sub + arr),是不是很诡异!

  • C中所有参数都是通过传值方式传递的。数组名作为参数时,通过传值方式得到的是指针的拷贝,虽然利用间接访问可以修改数组元素,但调用程序传递的实参并不受影响。

  • 一维数组作为形参无需指明元素数目,函数也并不会为数组参数分配内存空间。形参只是一个指针,可以与任何长度的数组匹配!

  • char message[] = "Hello"char *message = "Hello"

前者初始化了一个字符数组,而后者则是一个字符串常量。

  • 二维数组作为函数参数的传参方式有:void fun2(int (*mat)[10])void fun2(int mat[][10]) 两种,不能使用void fun2(int **mat) ,两者完全不是一回事。
  1. 字符串
  • strcpy

    字符串复制 char *strcpy(char *dst, char const *src);将参数src字符串复制到dst参数

必须保证目标字符数组的空间足以容纳需要复制的字符串,若字符串比数组长,多余的字符仍被复制,它将覆盖原来存储于数组后面的内存空间的值。

  • strcat

    字符串连接char *strcat(char *dst, char const *src);将一个字符串添加到另一个字符串的后面。

  • 字符串查找

函数原型功能
char *strchr(char const *str, int ch);在字符串str中查找字符ch第一次出现的位置,返回指向该位置的指针
char *strrchr(char const *str, int ch);在字符串str中查找字符ch最后一次出现的位置
char *strpbrk(char const *str, int ch);返回指向str中第一个匹配group中任何一个字符的字符位置
char *strstr(char const *s1, char const *s2);在s1中查找整个s2第一次出现的起始位置
size_t strspn(char const *str, char const *group);在字符串起始位置对字符计数
size_t strcspn(char const *str, char const *group);对字符串起始部分不与group中任何字符匹配的字符进行计数
char *strtok(char *str, char const *sep);找到str的下一个标记,并将其用NUL结尾,然后返回一个指向这个标记的指针
  1. 字节对齐

参考资料:

什么是字节对齐:各种数据类型按照一定的规则在空间上排列,这就是对齐。

为什么要字节对齐:

最重要的考虑是提高内存系统性能
前面我们也说到,计算机每次读写一个字节块,例如,假设计算机总是从内存中取8个字节,如果一个double数据的地址对齐成8的倍数,那么一个内存操作就可以读或者写,但是如果这个double数据的地址没有对齐,数据就可能被放在两个8字节块中,那么我们可能需要执行两次内存访问,才能读写完成。显然在这样的情况下,是低效的。所以需要字节对齐来提高内存系统性能。
在有些处理器中,如果需要未对齐的数据,可能不能够正确工作甚至crash

字节对齐原则:

数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合 :按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。

eg:

struct stu{
   char sex;
   int length;
   char name[10];
  };
struct stu my_stu;
//按4字节对齐,sex和name后面分别填充3个和2个字节,使整个结构体对齐。最后sizeof(my_stu)的值为20,而不是15
#pragma pack(1) /*1字节对齐*/
struct stu{
   char sex;
   int length;
   char name[10];
};
struct stu my_stu;
#pragma pack()/*还原默认对齐*/

//sizeof(my_stu)值为15
//使用伪指令#pragma pack(n)(n为字节对齐数)来使得结构间一字节对齐。
//这样做能够保证跨平台的结构大小一致,同时还节省了空间,但不幸的是,降低了效率。

可以用offsetof(type, member) 查看指定成员开始存储的位置偏离结构开始存储位置的字节数

struct Test1{
  int a;
  char b, c;
}test1;
printf("%d %d %d %d\n", sizeof(test1), offsetof(Test1, a), offsetof(Test1, b), offsetof(Test1, c));
//8 0 4 5

struct Test2{
  int a;
  char b;
  short c;
}test2;
printf("%d %d %d %d\n", sizeof(test2), offsetof(Test2, a), offsetof(Test2, b), offsetof(Test2, c));
//8 0 4 6

struct Test3{
  int a;
  char b;
  float c;
  short d;
}test3;
printf("%d %d %d %d %d\n", sizeof(test3), offsetof(Test3, a), offsetof(Test3, b), offsetof(Test3, c), offsetof(Test3, d));
//16 0 4 8 12
  1. 单链表

有序单链表的插入,使用二级指针后,无需引入额外的代码来处理特殊情况。

/*
** 插入到一个有序单链表。函数的参数是一个指向链表第一个节点的指针,以及一个需要插入的新值
*/
#include <stdlib.h>
#include <stdio.h>
#include "sll_node.h"

#define  FALSE 0
#define  TRUE  1

int
sll_insert( register Node **linkp, int new_value )
{
   register Node *current;
   register Node *new;
		/* 
   ** 寻找正确的插入位置,方法是按序访问链表,直到到达一个其值大于或等于
   ** 新值的节点。
   */
   while( ( current = *linkp ) != NULL &&
     current->value < new_value )
      linkp = &current->link;

   /* 
   ** 为新节点分配内存,并把新值存储到新节点中,如果内存分配失败,
   ** 函数返回FALSE。
   */
   new = (Node *)malloc( sizeof( Node ) );
   if( new == NULL )
     return FALSE;
   new->value = new_value;

   /* 
   ** 在链表中插入新节点,并返回TRUE。
   */
   new->link = current;
   *linkp = new;
   return TRUE;
}

从不同的操作找到共性,不然要编写额外的代码来处理特殊的情况!!

  1. 预处理器

主要任务: 删除注释、插入被#include 指令包含的文件的内容、定义和替换由#define 指令定义的符号以及确定代码的部分是否应该根据一些条件编译指令进行编译。

#define 指令可以把任何文本替换到程序中

#define reg register
#define do_forever for(;;)
#define CASE bread;case

//太长时, 可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠
//__FILE__ 文件名  __LINE__  行号
//相邻的字符串常量会自动连接成一个字符串
#define DEBUG_PRINT printf("File %s line %d" \
											"x = %d, y = %d, z = %d", \
											__FILE__, __LINE__, \
											x, y, z)

的声明方式: #define name(parameter-list) stuff

//宏声明的某个数的平方
//容易的错误是 #define SQUARE(x) x * x
//当计算a = 5; printf("%d\n", SQUARE(a+1)); 就会被替换成a+1*a+1 结果是11
//正确的写法是
#define SQUARE(x) (x) * (x)

在宏完整定义的两边加上括号,同时,在宏定义中每个参数的两边也要加上括号

宏与函数

宏用于执行简单的计算,如#define MAX(a, b) ((a) > (b) ? (a) : (b))

不用函数完成的原因是:1. 使用宏比使用函数在程序规模和速度方面都更胜一筹 2. 宏与类型无关,而函数的参数为特定类型,只能类型合适的表达式才能使用。3. 有一些任务,函数根本也无法完成。

#define MALLOC(n, type) \
					((type *)malloc((n) * sizeof(type)))
pi = MALLOC(25, int);
属性#define宏函数
代码长度每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数调用/返回的额外开销
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
参数求值参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多种求值过程。参数的副作用并不会造成任何特殊的问题
参数类型宏与类型无关。只要对参数的操作是合法的,它可以使用于任何参数类型函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的

#undef #undef name 移除定义

条件编译

#if constant-expression
		statements
#endif

使用条件编译可以避免多重包含

#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
/*
**All the stuff you want in the header file
*/
#endif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值