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就是别名,不用在后面再起别名。
不建议用宏定义来其别名,它纯粹是字符串替换,如果连续声明了两个变量就会有问题了。