C语言基础复习总结

时间:2022-08-01 12:34:47

C语言基础复习总结

大一学的C++,不过后来一直没用,大多还给老师了,最近看传智李明杰老师的ios课程的C语言入门部分,用了一周,每晚上看大概两小时左右,效果真是顶一学期的课,也许是因为有开发经验吧,废话少说,直接把总结贴出来了~

#include <stdio.h>

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

{

printf("Hello, World!\n");

return 0;

}

#include是预处理指令,在编译前把尖括号里的内容原封不动地拷贝到对应位置。.h是头文件,里面是库函数的声明(不是实现)。尖括号表明是系统自带的,会去系统目录找,双引号是自己的文件,会先在源程序当前目录找,找不到就去操作系统的path路径找,还找不到才去C函数库里找。

包含关系允许嵌套包含但是不允许递归包含(死循环)。

C语言语法不严格,main函数可以不写返回值,默认返回int,参数可以不要,可以不return。

C语言程序运行的过程:

1.把源代码翻译成目标代码。Xcode是64位编译器。编译成功后生成同名.obj文件(多个c文件对应多个obj文件)。

2.把C语言源文件之间的调用依赖以及C语言函数库链接进来,成为可执行的机器代码。在xcode下生成的是unix可执行文件。

在java当中方法定义没有顺序限制,但是在标准c当中只能后面的函数调用前面的,因为C是从上往下编译的,如果想放在前面,需要声明,声明可以省略参数名,只要类型:

#include <stdio.h>

int sum(int, int);

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

{

int c = sum(10,3);

printf("%d\n", c);

return 0;

}

int sum(int a, int b)

{

return a+b;

}

一般来说,会把函数声明和定义放在不同文件当中,比如把sum函数的声明放在test.h里,实现放在test.c里,然后在main函数之前引入:

#include "test.h"

反复引用同一个文件是没关系的,可以使用预编译指令做检查机制。但是不要导入.c文件,以免在链接时出现函数重复而报错,C不是面向对象的,函数名不能重复。

printf输出需要使用百分号占位符,.2f是保留两位小数,不是四舍五入。

// My age is 26, height is 1.55, name is 李明杰,sex is 'A'

printf("My age is %d, height is %.2f, name is %s,sex is ‘%c’\n", 26,1.55f,"李明杰",'A');

scanf是阻塞性的函数,等待标准设备输入,需要传变量的地址。

#include <stdio.h>

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

{

printf("请输入两个整数,用逗号隔开:");

int a,b;

scanf("%d,%d", &a, &b);  //传a的地址

printf("%d\n", a+b);

return 0;

}

C语言的类型分四类,一种是空类型viod,一种是int,float,double,char,一种是构造类型,比如数组,struct,union(基本没用),enum,另一种是指针类型void*。C是强类型语言,指定类型是为了分配适当大小的空间。Char类型不论多少位的编译器,都占一个字节。

C与java不同,局部变量没有初始化使用也不会报错,但默认值不一定是0,是随机数,所以不要不初始化。全局变量则会被默认初始化。

Char类型范围是-128到127,最好不用ascii码的值,直接用’a’,它不是unicode的,也不支持字符串。

类型修饰符:short,long,signed,unsigned,最常用的是修饰int,被修饰的int可以省略,比如只写long,与java不同,这并不代表这是long类型,而是int。不管什么编译器,int至少2个字节。

C语言里没有boolean,关系判断返回1或0的int值,没有-1!任何非0值都位真,只有0才是假,比如if(9)为真。

C语言可以用逗号连接多个表达式,它的返回值是最后一个表达式的值,下面的代码输出12。

#include <stdio.h>

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

{

int a=9;

int b=10;

int c;

c = (a=a+1,b=3*4);

printf("%d",c);

}

对于一个变量,变量存储单元的“第一个字节”的地址就是该变量的地址,取地址用&,返回数字,习惯用16进制。

#include <stdio.h>

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

{

char a = 'A';

int b = 66;

printf("%x\n", &a);

printf("%x", &b);

}

数组用来存放“同一种”类型的变量,不能用变量做长度,应该用常量,但是xcode不会报错。系统为数组分配的空间是连续的。

int ages[5];

printf("%d",sizeof(ages));

得到的长度是20,C语言数组名就代表数组地址,所以ages就是个常量,不能赋值。取数组地址的方法有:

printf("%d\n",&ages[0]);

printf("%d\n",ages);

数组可以初始化,比如

int a[2] = {8, 10};

放在后面的元素可以省略,但是可读性不好,比如

int a[2] = {8, };

如果后面已经定义了全部元素则长度可省略,比如

int a[] = {8, 10};

C语言传参是传值,但是传数组(指针)的话则不是,实际传的是地址,所以内容会被改变。

#include <stdio.h>

//接受数组参数,可以不写长度

void test(int array[])

{

array[0] = 9;

}

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

{

int a[3];

a[0] = 10;

printf("%d\n",a[0]);

test(a);    //数组名代表地址,传的是指针

printf("%d\n",a[0]);

}

二维数组是一维数组的集合,是由一维数组组成的一维数组。它在内存中是按行存储的,比如a[0][0]->a[0][1]->a[0][2]->a[1][0]。

取地址的方式有a,a[0],&a[0][0]。

初始化可以按行:

int a[2][3] = {{1,2,3},{4,5,6}};

也可以都写出来:

int a[2][3] = {1,2,3,4,5,6};

可以部分省略,默认为0:

int a[2][3] = {{1,,3},{4}};

可以省略行数,不能省略列数:

int a[][3] = {1,2,3,4,5};

C语言没有String类型,多个字符用字符数组存储,为了和普通字符数组区分,字符串数组用’\0’结尾,必须写,否则可能会内存溢出,它是一个ascii码为0的字符,是空操作符,表示什么也不干,所以“mj”的长度是3,不是2。通常用下面第二种初始化:

char s1[] = { 'm','j','\0' };

char s2[] = "mj";

打印的方式:

printf("%s\n", s2);

puts(s2);

放在尾部\0是因为输出过程会从字符串地址开始向后找第一个\0,所以必须要有结尾,没有结尾就会一直找下去,输出错乱的东西。

字符串输入的过程会自动在尾巴加\0,例子:

char s[20];

scanf("%s", s); //s就是地址,不需要&s

printf("%s", s);

但是gets是不安全的,原理同上,会把另一个字符串s2里的内容冲掉,例如:

char s2[] = "mj";

char s1[2];

gets(s1);

printf("%s\n", s1);

printf("%s\n", s2);

字符串本身就是数组,如果要存储多个字符串,则需要使用二维数组,比如char names[15][20]表示可以存15个名字。代码:

char names[2][20] = {{"jay"},{"jim"}};

字符串处理的两个方法:

//输出到控制台

putchar('a');

//等待用户输入

char c;

c = getchar();

字符串处理函数声明在string.h当中。

测量字符串的字符长度:

int len = strlen("李明杰");

printf("%d\n", len);

输出9,strlen返回字符串的字符数,不是长度,中文是3个字符。

字符串拷贝:

char left[10];

strcpy(left, "itcast");

printf("%s", left);

从右边的常量拷贝给左边的变量并且自动加\0。

拼接字符串:

char left[10] = {'m','j','\0'};

strcat(left, "ios");

printf("%s", left);

把右边的字符串,接在左边的后面,会去掉左边的“第一个”\0,但是要保证左边字符串的长度足够,否则会内存溢出。

字符串比较:

int delta = strcmp("abc","ABC");

printf("%d", delta);

返回左边减右边的差,一位一位地比ascii码,\0就是0.

指针变量用来保存一个特定类型的变量的地址,如:

char a;

char *b = &a;

*b = 'a';

printf("%c",a);

b是一个char*类变量指向a的地址,第三行*b当中的*是指针运算符,*b表示访问b的值(a的地址)所对应的存储空间。

指针类型所占用的空间只和编译器有关。

其中第二句可以拆分成两句:

char *b;

b = &a;

第二句不能写*b,*是访问符。

指针操作的两个错误:

//错误1:不要直接使用未分配的指针

char *p;

*p=10;

//错误2:不要给指针变量直接赋地址

p = 100;

交换a和b的例子:

void swap(int *v1, int *v2)

{

int temp = *v1;

*v1 = *v2;

*v2 = temp;

}

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

{

int a = 10;

int b = 9;

swap(&a,&b);

printf("%d %d\n",a,b);

}

因为默认会传临时变量,所以要传地址,参数表用指针类型,调用的时候就要传地址进去。交换的时候也要用星号来取指针变量里的地址对应的值。

用指针可以实现函数多返回值,类似c#的out参数。比如:

int sumAndMinus(int v1, int v2, int *p) {

*p = v1 - v2;

return v1 + v2;

}

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

{

int a = 10;

int b = 4;

int sum;

int minus;

sum = sumAndMinus(a,b,&minus);

printf("%d %d",sum,minus);

}

数组的名字就是它的地址,所以把指针指向数组的时候,后面两行代码是等价的:

int a[2];

int *p;

p = &a[0];

p = a;

可以用指针遍历数组,如下:

int a[3] = {1,2,3};

int *p = a;

for (int i=0; i<3; i++) {

printf("a[%d]=%d\n",i,*(p+i));

}

对于指针变量来说,p+i当中的i要看p指向的类型,比如指向一个两个字节的类型,i就是两个字节,不是纯粹加一个数字。上面的做法不会改变p所指向的内容,但是如果写*(p++)则会改。

另外,既然数组名就是p所指的,所以*(a+i)也可以,但是*(a++)不可以,数组的首地址是常量不能改。

像下面这样也可以,只是p最终位置改变了:

int *p = a;

for (int i=0; p < a + 3; i++, p++) {

printf("a[%d]=%d\n",i,*p);

}

如果参数表是数组,那么传数组名或指针都可以:

void change(char c[]) {

c[0] = 1;

}

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

{

char a[3];

change(a);

}

如果参数表是指针,那么也可以传指针:

void change(char *c) {

*c = 1;

}

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

{

char a[3];

change(a);

printf("%d\n", a[0]);

}

总之,如果形参是数组或指针,则可以传递数组名或指针。

对于一个字符串,遍历的方式有:

char s[7] = "itcast";

for (int i=0; s[i]!='\0'; i++) {

printf("%c\n", s[i]);

}

比较好理解的方法是:

char *p = "itcast";

for (; *p!='\0'; p++) {

printf("%c\n", *p);

}

第一种方式利用数组定义的是字符串变量,但是第二种方式用指针定义的是字符串常量,所以,第二种方法一旦用下面的方式来改写就错了:

char *p = "lmj";

*p = 'f';

总之,char a[] = “lmj”是变量,char *p = “lmj”是常量,严格来说前面应该加上const。

一个函数可以返回一个指针,比如:

char * test() {

return "itcast";

}

函数的名称就代表函数的地址,可以定义指向函数的指针,比如:

#include <stdio.h>

int sum(int a, int b) {

return a+b;

}

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

{

//定义一个特定返回值和参数表的指针p

//需要占位的是函数名,并且指向sum函数

int (*p)(int, int);

p = sum;

//利用指针变量p取出所指的函数,间接调用

int result = (*p)(1,2);

//也可以直接调用

int result2 = p(5,6);

printf("%d %d", result,result2);

}

可以把函数指针当做参数来使用,类似c#当中传递lambda表达式:

#include <stdio.h>

int calculate(int a, int b, int (*p)(int,int)) {

return p(a,b);

}

int sum(int a,int b){

return a+b;

}

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

{

int result = calculate(1,2, sum);

printf("%d", result);

}

预处理指令以#开头,是在编译之前执行的,三种常用的预处理指令分别是宏定义、文件包含和条件编译。

宏定义的功能是字符串替换,通常用来定义常量:

#define NUM 6

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

{

int a[NUM] = {1,2,3,4,5,6};

}

也可以使用带有参数的宏定义:

#define mul(a,b) ((a)*(b))

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

{

int a = mul(1,2);

printf("%d",a);

}

定义带有参数的宏最好把参数带上括号,因为它的实质是字符串替换。最外层最好也加一个括号,因为宏替换之后,它的整体还会和其他代码进行数学运算。

宏定义没有内存检测,纯粹是字符串替换,所以一些简单的计算,执行起来性能比函数要好。

条件编译指,某段代码,只让它在满足某种条件时才编译。

#include <stdio.h>

#define NUM 10

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

{

#if NUM > 0

printf("NUM大于0");

#elif NUM == 0

printf("NUM等于0");

#else

printf("NUM小于0");

#endif

return 0;

}

注意预处理指令里的宏NUM只能用预处理指令里定义的。

另外,可以根据有没有定义过宏来判断:

#define NUM 10

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

{

#ifdef NUM

printf("定义了");

#endif

#ifndef NUM

pringf("没定义");

#endif

return 0;

}

C的变量有不同的存储类型、生命周期和作用域。

局部变量只在函数内有效。全局变量被其他函数共享,从定义的位置到源代码结尾有效。

存储类型:有三个地方可以存变量:运行时堆栈、普通内存和硬件寄存器,它决定了变量的生命周期。

三个地方分别对应自动变量、静态变量和寄存器变量。

被关键字auto修饰的“局部”变量是自动变量,默认所有局部变量都是自动变量,基本不写auto。当执行到自动变量所在的函数时,自动变量会创建,离开函数时会销毁。

静态变量会在程序运行时创建,在程序结束后销毁。所有全局变量都是静态变量。另外有一种被static修饰的局部变量也是静态变量,这种修饰改变了它的生命周期,但是没改变作用域,它的创建时间也是在函数调用的时候。函数内的static变量可以在函数内部进行计数。

存储在硬件寄存器内的变量叫寄存器变量,它被register修饰,性能最高。只有自动变量才能存储在寄存器中,只限存储int、char和指针类型。如果寄存器已满,在运行时会自动转化为自动变量处理。寄存器变量通常是一些频繁使用的变量。寄存器变量的生命周期和自动变量一致。

允许被其他源文件调用的函数是外部函数,函数默认是外部函数,不允许有同名的外部函数,否则会有链接错误。

定义或者提前声明一个外部函数需要用extern,但是默认就是外部的,所以这个关键字一般不写。在C99标准当中,如果一个源文件里没有提前声明外部函数会报编译错误,但是xcode里不会出错。

不允许其他文件访问的函数是内部函数,不同源文件里允许有同名的内部函数。C的static和java中完全不一样,内部函数需要用static关键字修饰,这样在链接的时候其他源文件就无法找到这个static函数了。在同一个源文件中,声明一个static函数也需要加这个关键字。

C语言中一个函数不能使用在函数后面声明的变量,除非在函数前面声明,声明变量的关键字是extern,这个extern其实又可以省略:

extern int a;

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

{

a = 10;

return 0;

}

int a;

在一个函数内,如果用extern声明了一个变量,那么它使用的还是外部的变量,extern本来就是外部的意思,证明:

#include <stdio.h>

void test();

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

{

extern int a;

a=10;

test();

}

int a;

void test()

{

printf("%d", a);

}

在C语言中在不同的源文件当中存在同名的全局变量,则这些变量都代表同一个变量。允许被其他文件访问的变量叫外部变量,默认情况下定义的变量都是外部变量。想使用其他文件里的外部变量,需要在前面声明(extern可省)。注意,extern后面的变量,一定是声明,而不是定义!

用static修饰的全局变量叫做内部变量,外部访问不到这个变量(外部的extern声明语句无法看到内部的static变量)。如果说两个文件中的同名变量有一个或者都加了static,则它们不是一个变量。

结构体可以定义在方法内部或外部,如果在内部,则只能在函数内使用这个结构体:

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

{

//定义结构体类型

struct Student {

int age;

char *name;

float height;

};

//定义结构体变量

struct Student stu = { 27, "mj", 180.3};

printf("%d", stu.age);

}

也可以同时定义结构体并定义变量赋值,在这种情况下结构体的名字可以省略,因为没有意义,类似java的匿名方法:

struct {

int age;

char *name;

float height;

} stu = { 27, "mj", 1.8f};

结构体内可以包含别的结构体,但是不允许包含自己导致递归。需要注意的是,结构体的定义和初始化要写成一句,如果拆成两句会报错。

可以定义指针指向结构体,通过指针有两种方式取结构体内的值:

struct Student stu = { 27, "mj", 180.3};

struct Student *p = &stu;

int a =(*p).age;

int b = p->age;

把结构体当做参数传给函数,传的是值,不是地址,传地址需要指针:

struct Student {

int age;

};

void change(struct Student* stu) {

stu->age = 1;

}

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

{

struct Student stu = { 27, "mj", 180.3};

struct Student *p = &stu;

change(p);

printf("%d", stu.age);

}

C会把枚举当做整形常量,从0开始,定义枚举:

enum Season { spring, summer, autumn, winter };

enum Season s = spring;

也可以同时定义枚举变量并赋值,和结构体一样,枚举的名称也可以省略。

typedef关键字的作用是给数据类型定义一个别名,比如:

typedef int Integer;

typedef char* String;

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

{

Integer a = 10;

String name = "itcast";

}

可以给结构体起别名,这样定义变量的时候就能省略struct关键字了,这时建议用匿名的,因为没用:

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

{

typedef struct {

float x;

float y;

} CGPoint;

CGPoint p = {10, 10};

}

例子:给指向结构体的指针重命名,此时也可以匿名:

typedef struct Point{

float x;

float y;

} * PPoint;

struct Point point = {10,10};

PPoint pp = &point;

事实上最常用的定义结构体和枚举的方式是这样,表意非常明确——给一个匿名的结构体命名:

typedef struct {

float x;

float y;

} Point;

还可以给一个指向函数的指针一个别名:

int sum(int a, int b){

return a+b;

}

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

{

typedef int (*SumPoint)(int,int);

SumPoint p = sum;

p(1,2);

}

在这里SumPoint就是别名,不用在后面再起别名。

不建议用宏定义来其别名,它纯粹是字符串替换,如果连续声明了两个变量就会有问题了。

 
 
分类: IOS开发