c

learning C

c语言学习笔记

Posted by laosuan on July 28, 2020
- c language

C99规范

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf

main(int argc, char *argv[])

int argc:是整型变量,表示用户在运行程序时输入命令行参数的个数,argc至少为1,因为其中包括可执行程序名,即argv[0]中的内容。

char *argv[]:存放指向命令行参数的指针数组,数组从0开始,数组中内容如下 argv[0]:程序中的可执行文件的文件名或程序文件所在的路径。 argv[1]:程序在DOS命令中执行程序名后的第一个字符串。 argv[2]:执行程序名后的第二个字符串

#include <stdio.h>

int main(int argc, char *argv[])

{
    pintf("argc = %d\n",argc);
    printf("argv[0] = %s\n",argv[0]);
    printf("argv[1] = %s\n",argv[1]);
    printf("argv[2]" = %s\n,argv[2]);
}

运行结果:
[root@localhost 0216]# ./a.out aa bb cc    (命令行共有四个参数)
argc = 4
argv[0] = ./a.out       (可执行程序文件名)
argv[1] = aa
argv[2] = bb

字符串

puts输出

/* 将一个字符串(以'\0'结束的字符序列)输出到终端,str为数组名或字符指针变量.
 * 在用puts输出时将字符串结束标志'\0'转换成'\n',即输出完字符串后换行
 */
char str[] = "China\nBeijing";
puts(str)

gets输入

// 从终端输入一个字符串到字符数组
gets(str);

gets输入

// 从终端输入一个字符串到字符数组
gets(str);

strcat 字符串链接

// str1必须足够大,以便容纳连接后的新字符串
char str1[30] ="Ha Ha";
char str2[1] ="!";
printf("%s", strcat(str1, str2));

strcpy 字符串复制

// str1必须足够大,以便容纳str2, str2可以直接写成字符串常量“China"
char str1[10],char str2[] ="!";
strcpy(str1, str2);

//字符串数组不能直接赋值,故可以用以下方式赋值
char str1[];
strcpy(str1, "China");

strcmp 字符串比较

// 从左到右逐个字符相比,直到出现不同的字符或遇到'\0'为止,返回0或第一队不相同的字符串的比较结果,小写比大写大(返回正整数)
if(strcmp(str1, str2))

strlen 字符串长度

// 不包括'\0'在内的长度
strlen("China");

strlwr 字符串转小写 String LoWeRcase

struwr 字符串转大写 String UPpeRcase

文件

// 打开文件
char* file = "read.file"; // 文件名
File* fp = fopen(file, "r"); // r为只读文本文件,rb为只读二进制文件,ar为追加写入

// 关闭文件
fclose(fp);

写入读取文件

// 按ASC2读取文件 fscanf
static int read_number (lua_State *L, FILE *f) {
  lua_Number d;
  if (fscanf(f, LUA_NUMBER_SCAN, &d) == 1) {
    lua_pushnumber(L, d);
    return 1;
  }
  else {
    lua_pushnil(L);  /* "result" to be removed */
    return 0;  /* read fails */
  }

// 按二进制读取文件 fread
  *size = fread(lf->buff, 1, sizeof(lf->buff), lf->f);

// 读入一个字符 fgetc(fp)
// 练习1:从键盘输入一些字符,并逐个把它们送到磁盘上去,直到用户输入一个“#”为止.c程序设计-谭浩强 P338
// 练习2:将一个磁盘文件的信息复制到另一个磁盘文件中。今要求将上例建立的 filel.dat 文件中的内容复制到另一个磁盘文件 file2.dat 中。 c程序设计-谭浩强 P340
// 练习3:从键盘读入若个字符串,对它们按字母大小的顺序排序,然后把排好序的字符串送到磁盘文件中保存。 c程序设计-谭浩强 P343

static char *linenoiseNoTTY(void) {
    char *line = NULL;
    size_t len = 0, maxlen = 0;

    while(1) {
        if (len == maxlen) {
            if (maxlen == 0) maxlen = 16;
            maxlen *= 2;
            char *oldval = line;
            line = realloc(line,maxlen);
            if (line == NULL) {
                if (oldval) free(oldval);
                return NULL;
            }
        }
        int c = fgetc(stdin);//成功返回字符,失败返回EOF即-1
        if (c == EOF || c == '\n') {
            if (c == EOF && len == 0) {
                free(line);
                return NULL;
            } else {
                line[len] = '\0';
                return line;
            }
        } else {
            line[len] = c;
            len++;
        }
    }
}
   
   
// 按ASC2写入文件 fscanf
  fprintf(stderr, "Out of memory");

// 按二进制读取文件 fwrite
   retval = fwrite(buf,len,1,r->io.file.fp);
// 练习4:从键盘输入 10 个学生的有关数据,然后把它们转存到磁盘文件上去。c程序设计-谭浩强 P346
   
// 写入一个字符 fputs(ch, fp)
    fputs("\n", stderr);

指针

指针相减

  • 如果是正值,则表示在内存中p1比p2靠后
  • 如果是负值,则表示在内存中p1比p2靠前
  • 结果的数字表示, 两个地址在内存中间隔多少个指针类型的字节倍数

引用结构体成员变量

以下 3 种形式是等价的:

  • 结构体变量.成员名。
  • (*指针变量).成员名。
  • 指针变量->成员名。

指针判空

根据c99规范中6.5.3.3 Unary arithmetic operators Constraints第五条:

5 The result of the logical negation operator ! is 0 if the value of its operand compares unequal to 0, 1 if the value of its operand compares equal to 0. The result has type int. The expression !E is equivalent to (0==E).

!E和0==E等价

以及第5.59条:6.5.9 Equality operators

  • both operands have arithmetic type;

  • both operands are pointers to qualified or unqualified versions of compatible types;

  • one operand is a pointer to an object or incomplete type and the other is a pointer to a qualified or unqualified version of void; or

  • one operand is a pointer and the other is a null pointer constant.

    ==两边的操作数,可以一边是指针,一边是指向void类型的指针;或者,一边是指针一边是空指针常量。参见下面的具体描述:

以及第6.8.4.1 条if语句

The if statement Constraints The controlling expression of an if statement shall have scalar type. Semantics: In both forms, the first substatement is executed if the expression compares unequal to 0(true). In the else form, the second substatement is executed if the expression compares equal to 0(false). If the first substatement is reached via a label, the second substatement is not executed

不等于0则执行

所以: if(指针 != NULL) 等同与 if(指针 != 0) 等同于 if(指针)

申请变量的大小上限

一个由C/C++编译的程序占用的内存分为以下几个部分

  • 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap) :一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
  • 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  • 文字常量区:常量字符串就 是放在这里的。 程序结束后由系统释放
  • 程序代码区:存放函数体的二进制代码。

如下面代码所示:

int a = 0;//全局初始化区  
char *p1;//全局未初始化区  
main()  
{
    int b;// 栈  
    char s[] = "abc";// 栈  
    char *p2;//栈  
    char *p3 = "123456";//123456/0在常量区,p3在栈上。  
    static int c =0 //全局(静态)初始化区  
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);// 分配得来得10和20字节的区域就在堆区。  
    strcpy(p1, "123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"  
  • 函数内申请的变量,数组,是在栈(stack)中申请的一段连续的空间。栈的默认大小为2M或1M,开的比较小。
  • 全局变量,全局数组,静态数组(static)则是开在全局区(静态区)(static)。大小为2G,所以能够开的很大。
  • 而malloc、new出的空间,则是开在堆(heap)的一段不连续的空间。理论上则是硬盘大小。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;
//在本人环境中
int c[20000][20000]; //全局数组能开到20000*20000
int main()
{
    int b[100][100]; // 函数中二维数组最大能开100*100
    char a[4*518028]; // 函数中的char数组最大能开4*518028
    int b1[500000]; // int最大能开到518028。
    static int c[20000][20000]; //static能开到10^7*10^7,注意 static和 全局开的是同一块空间
    printf("1");
    return 0;

内存管理

memset

memset(void p, int c, int n) 初始化申请空间 ,将p指向的 n字节大小的空间,全部以字节为单位初始化成c, 例如:

char *p = (char *)malloc(sizeof(char) * 100);

memset(&p, 0, sizeof(char) * 100) //给*p指定的前100字节大小的内存空间设置为(只支持0, 1,以字节为单位赋初始值)

malloc

void * malloc(int n) 传入申请空间大小,单位字节,并返回该控件的内存地址

申请一块内存空间,并未初始化,一般会与memset(void *p, int c, int n)结合初始化内存空间

现在系统抹去内存区域只是把标记除去,并不会吧内存地址清楚为0,因此必须使用memset来初始化申请空间,否则申请的空间是内存中的默认非空白空间,可能会乱码或者与想象值不同

注意: malloc函数分配的空间也是未初始化的。

calloc

calloc()函数是malloc的简单包装。它的主要优点是把动态分配的内存清零。

void *calloc(int n, int size) 

该函数与malloc函数的一个显著不同时是,calloc函数得到的内存空间是经过初始化的,其内容全为0。calloc函数适合为数组申请空间,可以将size设置为数组元素的空间长度,将n设置为数组的容量。

注意:relloc函数分配的空间也是已初始化的。

realloc

realloc函数的功能比malloc函数和calloc函数的功能更为丰富,可以实现内存分配和内存释放的功能,其函数声明如下:

void * realloc(void * p,int n);

其中,指针p必须为指向堆内存空间的指针,即由malloc函数、calloc函数或realloc函数分配空间的指针。realloc函数将指针 p指向的内存块的大小改变为n字节。如果n小于或等于p之前指向的空间大小,那么。保持原有状态不变。如果n大于原来p之前指向的空间大小,那么,系统将重新为p从堆上分配一块大小为n的内存空间,同时,将原来指向空间的内容依次复制到新的内存空间上,p之前指向的空间被释放。

注意:relloc函数分配的空间也是未初始化的, 根据新指定大小与原来对比,来进行重分配,小于地址不变,大于则复制并重新分配,释放之空间。

free

void free(void * p); 释放p申请的空间,并设置为null

typedef的用法总结

用途一:

定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。比如:

char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针,

// 和一个字符变量;

以下则可行:

typedef char* PCHAR;

PCHAR pa, pb;  

这种用法很有用,特别是char* pa, pb的定义,初学者往往认为是定义了两个字符型指针,其实不是,而用typedef char* PCHAR就不会出现这样的问题,减少了错误的发生。

用途二: 用在旧的C代码中,帮助struct。以前的代码中,声明struct新对象时,必须要带上struct,即形式为: struct 结构名对象名,如:

struct tagPOINT1

 {
    int x;

    int y;
 };

struct tagPOINT1 p1;

而在C++中,则可以直接写:结构名对象名,即:tagPOINT1 p1;

typedef struct tagPOINT
{
    int x;

    int y;
}POINT;

POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时

用途三:

用typedef来定义与平台无关的类型。

比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:

typedef long double REAL;

在不支持 long double 的平台二上,改为:

typedef double REAL;

在连 double 都不支持的平台三上,改为:

typedef float REAL;

也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。

标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健。

用途四:

为复杂的声明定义一个新的简单的别名。方法是:在原来的声明里逐步用别名替换一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版。

举例: 原声明:

void (*b[10]) (void (*)());

变量名为b,先替换右边部分括号里的,pFunParam为别名一:

typedef void (*pFunParam)();

再替换左边的变量b,pFunx为别名二:

typedef void (*pFunx)(pFunParam);

原声明的最简化版:

pFunx b[10];

原声明:

doube(*)() (*e)[9];

变量名为e,先替换左边部分,pFuny为别名一:

typedef double(*pFuny)();

再替换右边的变量e,pFunParamy为别名二

typedef pFuny (*pFunParamy)[9];

原声明的最简化版:

pFunParamy e;

理解复杂声明可用的“右左法则”:从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。

举例:

int (*func)(int *p);

首先找到变量名func,外面有一对圆括号,而且左边是一个号,这说明func是一个指针;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明(func)是一个函数,所以func是一个指向这类函数的指针,即函数指针,这类函数具有int类型的形参,返回值类型是int。int (func[5])(int );func右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个,说明func的元素是指针(注意这里的不是修饰func,而是修饰func[5]的,原因是[]运算符优先级比高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。

这种用法是比较复杂的,出现的频率也不少,往往在看到这样的用法却不能理解,相信以上的解释能有所帮助。

#typedef vs #define

#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

  • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

下面是 #define 的最简单的用法:

实例

#include <stdio.h>   
#define TRUE  1 
#define FALSE 0   
int main( ) {    
  printf( "TRUE 的值: %d\n", TRUE);    
  printf( "FALSE 的值: %d\n", FALSE);      
  return 0; 
}

当上面的代码被编译和执行时,它会产生下列结果:

TRUE 的值: 1
FALSE 的值: 0

案例一:

通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:

typedef char *pStr1;

#define pStr2 char *;

pStr1 s1, s2;

pStr2 s3, s4;

在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们

所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一

个类型起新名字。

案例二:

下面的代码中编译器会报一个错误,你知道是哪个语句错了吗?

typedef char * pStr;

char string[4] = "abc";

const char *p1 = string;

const pStr p2 = string;

p1++;

p2++;

  是p2++出错了。这个问题再一次提醒我们:typedef和#define不同,它不是简单的

文本替换。上述代码中const pStr p2并不等于const char * p2。const pStr p2和

const long x本质上没有区别,都是对变量进行只读限制,只不过此处变量p2的数据类

型是我们自己定义的而不是系统固有类型而已。因此,const pStr p2的含义是:限定数

据类型为char *的变量p2为只读,因此p2++错误。

C 头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

引用头文件的语法

使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:

#include <file>

这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

#include "file"

这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

引用头文件的操作

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 header.h,如下:

char *test (void);

和一个使用了头文件的主程序 program.c,如下:

int x;
#include "header.h"

int main (void)
{
   puts (test ());
}

编译器会看到如下的代码信息:

int x;
char *test (void);

int main (void)
{
   puts (test ());
}

只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,其中“ifndef”和“define”后面跟的是相同的“标识”,通常和头文件名相同,所有字母均大写并把点号改为下划线即可 如下:

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

下面给一个#ifndef/#define/#endif的格式:

#ifndef A_H意思是"if not define a.h"  如果不存在a.h

接着的语句应该#define A_H  就引入a.h

最后一句应该写#endif   否则不需要引入

ifndef GRAPHICS_H // 防止graphics.h被重复引用 
define GRAPHICS_H 

关于自增和自减

一直以来,++ 和 –– 语法浪费了太多人的时间。说句实在话,++ 和 –– 在C语言中根本就不重要,除了表达简练外,真的没有什么其他好处了。 简单地说:++i 和 i++ 在单独使用时,都表示 i=i+1;––i 和 i–– 在单独使用时,都表示 i=i–1。

如果实在搞不明白 ++ 和 –– 是怎么回事,那也不是什么天塌下来的事情。

a=++i;

完全可以写成

i++; a=i;
a=i++;

也完全可以写成

a=i; i++;

而且,这也是一种很好的程序风格。

赋值运算返回左值

https://img-blog.csdn.net/20170413213454939?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd3VfbmFuX25hbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast

二级指针

二级指针与取地址运算符