C 语言概述
一、基本概念
- C 是一门 面向过程 强类型 静态 编译型 高级语言
二、hello world
1、gcc 安装使用
- 编译 C 程序请先安装 GCC 编译器
- gcc windows使用:
gcc haha.c
即可编译程序,默认的可执行程序的名称为 a.exegcc haha.c -o haha.exe
指定程序的名称haha.exe
控制台直接输入这个就能执行程序getchar()
直接打开可执行程序控制台一闪而过的解决方法
2、hello world demo
#include <stdio.h>
int main()
{
/* 我的第一个 C 程序 */
printf("Hello, World! \n");
return 0;
}
C 基础知识
一、基本语法
- C 的令牌(Tokens)是最小语法元素,可以是关键字、标识符、常量、字符串值,或者是一个符号;
- C语句也是以分号结束,且分号不能少;
- 注释和 java 一样(单行和多行);
- 标识符命名规则也和 java 一致;
- C语言对大小写敏感;
二、数据类型
1、整数类型
- char-----unsigned char(1)
- short-----unsigned short(2)
- int-----unsigned int(4)
- long-----unsigned long(8)
- long int------long long
2、浮点类型
- float(4)----double(8)----long double(16)
3、void 类型
- 函数的返回值和参数为空都可以用 void;
- 指针指向 void:类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。
4、类型转换
- 自动转换(隐式转换),基本的方向如下:
int -> unsigned int -> long -> unsigned long -> unsigned long long -> float -> double -> long double - 有时候需要进行 强制转换,下面的例子是否进行强制转换得到的结果是不同的
#include <stdio.h>
int main()
{
int sum = 17, count = 5;
double mean;
mean = (double)sum / count;
printf("Value of mean : %f\n", mean ); // result is 3.4
mean = sum / count;
printf("Value of mean : %f\n", mean ); //result is 3.0
}
三、变量、常量、存储类和运算符
1、变量
- 变量的申明和定义:定义是需要分配存储空间的,而仅仅申明不需要
extern int i; //声明,不是定义
int i; //声明,也是定义
- 关于左值和右值
- 左值(lvalue):指向内存位置的表达式被称为左值表达式;
- 右值(rvalue):指的是存储在内存中某些地址的数值;
- 总结:变量是左值,可以出现在等号的左右两边;数字是右值,不能出现在赋值号的左边;
2、常量
- 四种常量
- 整数常量和浮点常量:
- 字符常量和字符串常量(区分单括号和双括号):转义字符的用法和其他的语言差不多;
- 常量定义
#define LENGTH 10
const int WIDTH = 5;
3、存储类(修饰符)
- auto:局部变量默认的存储类,只能修饰局部变量;
- register:修饰的还是局部变量,但是这个局部变量比较特殊,一般是存在寄存器(CPU)中的(不是一定的),不能使用一元的 & 运算符(不在内存中);
- static:修饰局部变量,在程序的整个生命周期这个局部变量都不会销毁;修饰全局变量,会使变量的作用域限制在声明它的文件内(在另外的文件中用 extern 也无法访问),只要在同一个文件中怎么调用都行;
- extern:用来在一个文件中声明一个全局变量或函数,还可以用来引用其他文件的非 static 的全局变量和函数;
4、运算符
- 算数运算符也是支持 ++ 和 – 的,用法和 java 一样;
- 位移运算比 java 少了无符号的右移,其余一致;
- 其他运算符:sizeof------&取地址-------*a 指向一个变量;
四、语句和函数
1、语句
- 判断:if—else 和 switch(break),用法和 java 基本是一样;
- 循环:while、for 和 do–while,用法也是和 java 一致的,也支持continue 和 break,还有goto(跳转到标记的行,不建议使用);
2、函数
- 函数声明:int max(int num1, int num2);使用在前面,定义在后面的话,需要在使用之前进行函数的声明;
- 函数传参:
- 值传递:不会改变实参的值
- 引用传递:通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
3、作用域
- 全局变量:全局变量保存在内存的全局存储区中,占用静态的存储单元;自动初始化;
- 局部变量:局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元;需要手动初始化;
- 函数形参:被当作该函数的局部变量;
数据类型(扩展)
数组、枚举、字符串、指针、结构体、共用体
一、数组
1、基本使用
- 数组的声明和初始化如下,访问还是通过下标访问;
- 数组容量的获取可以使用:
double haha[4];int len = sizeof(haha)/sizeof(double);
- 多维数组的使用和 java 没有太大的差异;
- 数组名 balance 和 &balance 的值是一样的,但意义不同,前者是数组的首元素地址,后者是整个数组的地址;一般情况等同来用不会有太大的问题;
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
2、数组作函数参数和返回值
作返回值
- 返回数组一定要将函数内的数组声明为 static;
- 不允许返回一个完整的数组,只能返回一个指针(数组名);
- 函数的定义的时候返回值也要写指针;
int main(){
int* getArr();
int* arr = getArr();
int i;
for(i=0;i <2;i++){
printf("%d\n",arr[i]);
}
getchar();
return 0;
}
int* getArr(){
static int arr[] = {12,45};
return arr;
}
作参数
- 类似指针作形参 void myFunction(int *param),可以传递一个数组(这里的变量名 param 和数组名实际代表的都是地址,可以等同看待);
int main(){
int balance[]={12,23,10};
int sum = add(&balance,3);
printf("sum is %d \n",sum);
getchar();
return 0;
}
int add(int arr[],int len){
int sum =0;
int i;
for(i=0;i < len;i++){
sum += arr[i];
}
return sum;
}
int add(int* arr,int len){
int sum =0;
int i;
for(i=0;i < len;i++){
sum += arr[i];
}
return sum;
}
C语言随机数使用:srand( (unsigned)time( NULL ) );rand();
计算机内存的基本单位是 byte,而不是 bit;
3、指向数组的指针和指针数组
指向数组的指针
- 在如下定义的前提下,*(balance + 4) 是一种访问 balance[4] 数据的合法方式?
int test[]={12,34,56};
int (*p)[3];
p = test;
printf("(*p)[1]=%d\n",(*p)[1]);
printf("p=%d\n",p);
printf("*p=%d\n",*p);
指针数组
- 首先这是一个数组,其次这个数组的元素都是指针,存的是指向其他变量的地址;
- 可以用一个指向字符的指针数组来存储一个字符串列表,使用如下:
int *p[2] ;
int a=11,b=22;
p[0] = &a;
p[1] = &b;
printf("[%d,%d]\n",*p[0],*p[1]);
指向指针的指针
- 多级的间接寻址的方式,会形成指针链
int var;
int *ptr;
int **pptr;
var = 3000;
ptr = &var;
pptr = &ptr;
printf("Value of var = %d\n", var );
printf("Value available at *ptr = %d\n", *ptr );
printf("Value available at **pptr = %d\n", **pptr);
二、枚举
1、枚举的定义和使用
- 枚举的本质就是一些离散的整数值;
- 枚举的定义和使用如下所示:
enum DAY{MON=1, TUE, WED, THU, FRI, SAT, SUN};
enum DAY day;
2、其他说明
- 枚举的遍历:值连续的枚举可以循环遍历;
- 整数转换为枚举类型:(enum DAY)1;
- 枚举在 switch 语句中的应用(java 中也可以);
三、指针
1、指针是什么
- 指针是一个特殊的变量,这个变量的值是另一个变量的地址
2、指针怎么用
#include <stdio.h>
int main ()
{
int var = 20; /* 实际变量的声明 */
int *ip; /* 指针变量的声明 */
ip = &var; /* 在指针变量中存储 var 的地址 */
printf("Address of var variable: %p\n", &var );
/* 在指针变量中存储的地址 */
printf("Address stored in ip variable: %p\n", ip );
/* 使用指针访问值 */
printf("Value of *ip variable: %d\n", *ip );
return 0;
}
3、指针使用细节
- 指针的递增递减和比较:指针支持 ++ - - 等算数操作和 <> 等比较运算;
- 函数指针:指向函数的指针,给函数指针赋值时,可以用&fun或直接用函数名fun,二者基本是等效的,都能取到函数的地址;
回调函数
- 函数指针变量可以作为某个函数的参数来使用的
- 回调函数就是一个通过函数指针调用的函数
- 回调函数是由别人的函数执行时调用你实现的函数
#include <stdlib.h>
#include <stdio.h>
// 回调函数
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}
// 获取随机值
int getNextRandomValue(void)
{
return rand();
}
int main(void)
{
int myarray[10];
populate_array(myarray, 10, getNextRandomValue);
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
四、字符串
1、字符串的本质和定义
- 字符串的本质就是字符数组,其定义的形式是这样的:char greeting[] = “Hello”
2、字符串常用函数
- 如下的函数都是在 c 的标准库<string.h>中的
- 需要注意的是c语言中字符串是不能直接赋值的,要使用strcpy函数来赋值(定义初始化的时候例外);
五、结构体
有点类似于 java 中的类的概念
1、结构体的定义、初始化和访问
- 数组是用来存储相同类型的数据的,而结构体是用来组织相关联的不同类型的数据的
- 结构体成员的访问方式和 java 中类成员的访问方式是类似的(book.title)
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 语言", "test", "编程语言", 123456};
2、结构体做函数参数和指向结构的指针
- 结构体做函数的参数的使用和其他类型做函数参数没啥不同;
- 指向结构体的指针使用:定义和通过指针访问结构体的成员
struct Book book;
struct Book *b1 = &book;
book.book_id = 123;
strcpy(book.title, "love");
strcpy(book.author, "allen");
printf("%s\n",b1->author);
3、位域的概念
- 位域的个人的理解就是对字节(byte)进行更加精确的位(bit)划分,利用划分出来的位段来存储数据,这样可以节省存储空间,并且便于处理;
- 位域的定义如下,访问按照结构体成员的一般方式即可访问
struct bs{
int a:8;
int b:2;
int c:6;
}data;
关于位域的几点说明
- 位域在本质上就是一种结构类型,不过其成员是按二进位分配的;
- 位域可以是无名位域,只用来作填充或调整位置,是不能使用的;
- 关于位域的跨字节问题:很多教程说位域不能跨字节,但是实测是可以的,限制是不能超过原有类型的字节数(int 位域划分就不能超过 32 位);
- 位域的赋值超过位数限定范围不会报错但是数值会出现重置的现象(4位的位域赋值 17,打印出来的值就是1);
4、用结构体实现类似于面向对象的编程方式
int max(int,int);
struct ren{
int (*mymax)(int,int);
char name[50];
} ren1 = {max,"haha"};
int main(void)
{
struct ren ren2;
strcpy(ren2.name,"ren2");
ren2.mymax = max;
printf("ren2 name = %s\n",ren2.name);
printf("ren2 max = %d\n",ren2.mymax(34,1000));
return 0;
}
int max(int a,int b){
return a>b?a:b;
}
六、共用体
1、共用体说明
- 共用体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型;
- 可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值,且占用的空间是按成员中占用空间最大的那个来算的;
- 共用体变量同时使用会导致后面的覆盖前面的,使得前面的赋值出现损失,所以不要同时使用;
- 共用体的定义和使用和结构体是差不多的:
union Data
{
int i;
float f;
char str[20];
};
union Data haha;
haha.i = 10;
2、关于 typedef
- typedef 可以用来为内置的数据类型和自定义的数据类型取别名,定义之后使用该类型可以直接用别名替代;
- typedef 和 #define:
- typedef 仅限于为变量类型定义别名,#define 既可以为变量类型定义别名,也可以为常量定义别名;
- typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的;
输入&输出
一、控制台输入输出
- 需要标准库的支持<stdio.h>
- printf 和 scanf:格式化输出
- scanf("%s %d", &str, &i) 期待你的输入的类型和需要的类型相同,否则有可能报错;
- scanf 遇到空格就会停止读取,所以 “this is test” 对 scanf 来说是三个字符串;
- getchar 和 putchar:一个字符的输入和输出
- gets 和 puts:一行一行地读写,需要提供一个缓冲区
char str[100];
gets( str );
puts( str );
二、文件读写
- 文件打开(fopen)
- 文件关闭(fclose)
- 文件写入(写字符:fputc 写字符串:fputs 和 fprintf)
- 文件读取(读字符:fgetc 读字符串:fgets 和 fscanf ----fgets 按行读取,而 fscanf 是以空格作为结束的)
- 二进制输入输出函数(fread 和 fwrite)
int main(){
FILE *f ;
f = fopen("aa.txt","r");
//fprintf(f,"%s %s %s %d","we","are","in",2014);
/*int i = 0;
while(i < 5){
fputs("\nhello hello what is your name?",f);
i ++;
} */
char buffer[255];
while(!feof(f)){
fgets(buffer,255,f);
//fscanf(f,"%s ",buffer);
printf("%s",buffer);
}
fclose(f);
return 0;
}
预处理器和头文件
一、预处理器和宏
1、二者的概念
- 预处理器就是一个文本替换的工具;
- C 语言中的宏就是实现文本替换功能的代码;
2、预处理器指令和运算符
- 常见的预处理器指令如下:
- 预处理器运算符:续行符(\)、字符串常量化运算符(#)、标记粘贴运算符(##)、defined() 运算符
3、预定义宏和参数化宏
- 预定义宏常用的如下,不能直接修改:
- DATE: 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量
- TIME :当前时间,一个以 “HH:MM:SS” 格式表示的字符常量
- FILE :这会包含当前文件名,一个字符串常量
- 可以使用参数化的宏来模拟函数,必须要注意的是宏的本质就是用来文本替换的,所以下面的实例中括号绝对不能少,少了可能出错:
#define square(x) ((x) * (x))
//二者实现的功能是一样的
int square(int x) {
return x * x;
}
二、头文件
这里的用法类似于java中的import
1、基本语法
#include <file> 引用系统头文件
#include "test.c" 引用用户头文件
2、只引用一次
#ifndef HEADER_FILE
#define HEADER_FILE
int max(int x,int y){return x;}
#endif
3、条件引用
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif
错误&递归&可变参&内存管理&命令行参数
一、错误处理
- C 语言没有提供类似 java 的错误处理的机制,只能在错误发生的时候输出一些错误的信息;
- 在发生错误时,大多数函数会返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量
- C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的错误消息
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno ;
int main ()
{
FILE * pf;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errno));
}
else
{
fclose (pf);
}
return 0;
}
程序的退出状态:程序正常退出exit(0);程序发生异常exit(-1);
二、递归
- 所谓的递归就是函数自己调用自己,最经典的例子就是斐波那契数列的实现;
- 递归一定要有退出条件,不然会形成死循环,导致程序出错;
- 递归的次数不可过多,次数过多的话效率十分低下,还容易导致栈的溢出;
#include <stdio.h>
int fibonaci(int i)
{
if(i == 0)
{
return 0;
}
if(i == 1)
{
return 1;
}
return fibonaci(i-1) + fibonaci(i-2);
}
int main()
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d\t\n", fibonaci(i));
}
return 0;
}
三、可变参
1、可变参的使用
- 依赖于 C 的标准库:#include <stdarg.h>
- 库变量:va_list(参数列表)
- 库函数:va_arg(访问参数的某个项)、va_start(初始化valist)、va_end(清理valist内存)
2、详细使用示例
#include <stdio.h>
#include <stdarg.h>
double getAve(int num,...){
va_list valist;
va_start(valist,num);
double sum =0;
int i;
while(i < num){
sum += va_arg(valist,int);
i ++;
}
va_end(valist);
return sum/num;
}
int main()
{
printf("ave is %f\n",getAve(3,10,5,15));
return 0;
}
四、内存管理
1、内存管理基本方法
- 要依赖于 C 的标准库:#include <stdlib.h>
- 动态分配内存:calloc(int num,int size)、malloc(int num)
- 释放内存:free(void *address)
- 重新分配内存:realloc(void *address, int newsize)
2、详细使用示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char name[100];
strcpy(name,"allen");
char *des;
des = malloc(50*sizeof(char));
if(des == NULL){
fprintf(stderr,"can not allocate memery");
}else{
strcpy(des,"allen is a good student\t");
}
des = realloc(des,100*sizeof(char));
if(des == NULL){
fprintf(stderr,"can not allocate memery");
}else{
strcat(des,"allen is 18 and he is very good-looking");
}
printf("%s\n",name);
printf("%s\n",des);
free(des);
return 0;
}
五、命令行参数
- int main( int argc, char *argv[] ):argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数;
- argv[0] 存储程序的名称,argv[1] 是一个指向第一个命令行参数的指针;
- 多个命令行参数之间用空格分隔,如果参数本身带有空格,应把参数放置在双引号或单引号内部;
int main(int num,char *haha[])
{
int i =0;
while(i <num){
printf("%s\n",haha[i]);
i ++;
}
return 0;
}
标准库参考:https://www.runoob.com/cprogramming/c-standard-library.html