CPP 基础
C 语言基础
C 语言关键字
char, short, int, long, float, double, struct, union, enum, void, const, signed, unsigned, sizeof, typedef, auto, static, register, extern, if, else, switch, case, default, for, while, do, break, continue, return
基本数据类型
char, short, int, long, long long, float, double, void, struct, union, enum
等。- 在基本数据类型前加
unsigned
表示无符号类型,最高位不作为符号位,而是数值位。 - 整数默认是
int
,小数默认是double
。所以定义long
类型时最好在数字末尾加l, L
,定义float
类型最好在数字末尾加f, F
基本数据类型 | 字节数 | 范围 |
---|---|---|
char | 1 | -128~127 |
short | 2 | -32768~32767 |
int | 2 或 4 | -231~231-1 |
long | 4 | -231~231-1 |
long long | 8 | -264~264-1 |
float | 4 | – |
double | 8 | – |
变量的存储类型
变量除了有数据类型,还有另外一个重要属性,存储类型。数据类型决定了变量的大小,而存储类型决定了变量的生命周期和可见性。
C 语言中,变量的存储类型有 4 种,auto, static, register, extern
,一般使用的只有前两个。
auto
,默认存储类型。auto 只能用在函数内,修饰局部变量。static
,全局变量默认以 static 修饰。当修饰局部变量时,必须且只能被初始化一次。register
,用于定义存储在寄存器中而不是 RAM 中的局部变量,用于运算要求高的变量。extern
,用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。
算数运算符
+, -, *, /, %, ++, --, <, <=, >, >=, ==, !=, &&, ||, !, &, |, ~, ^, >>, <<, +=, sizeof, ., [], *, (), ?:, 强制类型转换
a^b=c, 则a^c=b, b^c=a
,这一规律广泛运用于密码学。a>>n,相当于a/(2^n)
- 有符号数,右移时最高位补符号位。
- 无符号数,右移时最高位补 0。
a<<n,相当于a*2^n
,最低位直接补 0。
数组与字符串
int a[10]={1, 2, 3}
,前 4 个数分别为 1, 2, 3,后面的数均初始化为 0。- 字符串的末尾必须是
\0
,占一个字节。
指针
变量都存放在从某个内存地址开始的连续的若干个字节中。
- 开始的内存地址:指针。
- 连续的若干字节:指针对应类型的字节长度。
int *p = (int *)1000
,指针指向内存地址为 1000 的位置,管辖后面连续的 4 个字节。
指针变量的大小
指针变量是一种数据类型,它的大小均为sizeof(T *) = 4
,故无论指针变量指向的是何种数据类型,它的大小均为 4 字节。
这是因为当今主流的 CPU 为 32 位,寻址范围是 2^32=4G,所以指针变量只需要 32 位即可。当然对于 64 位 CPU,编译器产生的指针长度也就是sizeof(T *) = 8
。
主机字节顺序
- 大端:低地址存放高位数据,这种最为直观。
- 小端:低地址存放低位数据,在程序,网络中,一般采用的是小端。
例如,将0x1234bacd
以不同的方式存储:
地址 | 大端 | 小端 |
---|---|---|
0x0000 | 12 | cd |
0x0001 | 34 | ba |
0x0002 | ab | 34 |
0x0003 | cd | 12 |
unsigned short a = 0x1234;
char *p = (char *)&a;
for(int i=0;i<2;i++){
printf("%2x\n", *(p+i));
}
如果
short a = 0xf234
,在输出首字节的0xf2
时,会出现fffffff2
而不是f2
,猜测可能是由于printf()
函数在输出时将char
自动转换成了 4 字节的int
造成的,因为%x
对应的就是整型,整型默认是int
。
指针运算
- 同类型的指针可以比较。
- 同类型的指针可以相减。
p2-p1 = (p2-p1)/sizeof(T)
- 指针可以和整型常量相加,相减。
p ± n = p ± n*sizeof(T)
- 指针可以自增,自减。
指针与动态分配内存
- 分配内存:
int *p = new int, int *p = new int[10]
- 释放内存:
delete p, delete[] p
指针类型
- 空指针,也称野指针,未指向任何地址的指针。
p = NULL
- void 指针,指向 void 类型的指针,可以强制转换为任意类型指针。
- 指向指针的指针,
int **p
- 数组和字符串与指针,指向数组和字符串首地址的指针。对于二维数组,还区分行指针和列指针,行指针也就是指向指针的指针。
- 指向函数的指针,该类指针指向某一类型的函数。
- 常量指针,指针的指向可以改,但是指向的值不可以改。
const int *p = &a
- 指针常量,指针的指向不可以改,但是指向的值可以改。
int * const p = &a
int add(int a, int b)
{
return a+b;
}
int main()
{
int (*p)(int, int); // 定义了一个指向某一类型的函数指针p
p = add;
cout << p(1, 2) << endl;
}
结构体
定义结构体
stuct student
{
string name;
int age;
};
使用结构体
// 初始化
student s1;
student s2 = {"Tom", 18};
cout << s2.name << endl; // 变量
student s3 = {
name : "Alice",
age : 16,
};
student *p = &s3;
cout << p->name << endl; // 指针
联合体
定义联合体
union data
{
int a;
float b;
char c[20];
}
- 联合体的大小以最大的类型为主,也就是说上面定义的
data
联合体的大小为 20 字节。
使用联合体
联合体在同一时间只使用一个变量,因为他们共用一块最大的内存。
union data data1;
data1.i = 10;
data1.f = 220.5; // 此时会覆盖掉数据10
strcpy( data1.str, "C Programming"); // 此时会覆盖掉数据220.5
printf( "data.str : %s\n", data1.str);
枚举类型
// 第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1
enum DAY
{
SUN, MON, TUE, WED, THU, FRI, SAT
};
// 可以为单独某个值指定数值,后续枚举成员的值仍在这个基础上加 1
// 如下,spring = 0, summer = 3, autumn = 4, winter = 5
enum season {spring, summer=3, autumn, winter};
预处理
引用头文件
#include <stdio.h>
#include "mymath.h"
宏定义与条件编译
#define, #undef, #ifdef, #ifndef, #if, #else, #elif, #endif
带参数宏
#define add(a, b) a+b
头文件的多次引用
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,一般使用#idndef xx
来解决这一问题,放在头文件中。
#ifndef _MYMATH_H
#define _MYMATH_H
// xxx
#endif
读写操作
标准输入输出
printf()
,标准输出到stdout, stderr
scanf()
,标准输入到stdin
int fgetc(FILE *)
,返回输入的一个字符,EOF
时返回-1。int fputc(int c, FILE *)
,输出一个字符。char *fgets(char *s, int n, FILE *)
,输入一段文本以回车结束。int fputs(const char *s, FILE *)
,输出一段文本。sscanf(buf, "%d%d", &a, &b)
,从数组中读。sprintf(buf, "%d%d", a, b)
,写入数组。fscanf(FILE *, 其他)
,从文件读。fprintf(FILE *, 其他)
,写入文件。
重定向
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
文件读写
打开文件
FILE *fopen(const char * filename, const char * mode);
,其中,filename
是文件路径的字符串,mode
是打开模式,如下:
模式 | 含义 |
---|---|
r | 只读 |
w | 覆盖写,若文件不存在则创建 |
a | 追加写,若文件不存在则创建 |
xb | 操作的是二进制 |
x+ | 允许读写 |
读写文件
int fputc( int c, FILE *fp );
,返回输出的字符,否则 EOF。int fputs( const char *s, FILE *fp );
,成功返回一个非负值,否则 EOF。int fgetc( FILE * fp );
,返回读取的字符,否则 EOF。char *fgets( char *buf, int n, FILE *fp );
fscanf(FILE *, 其他)
,从文件读。fprintf(FILE *, 其他)
,写入文件。size_t fread(void *buf, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
,返回已读取字节数。size_t fwrite(const void *buf, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
,返回已写入字节数。
int fseek(FILE *stream, long offset, int whence);
,移动文件指针到指定位置。offset
是相对于 whence 的偏移位置,正数往后,负数往前。whence
可以是SEEK_SET, SEEK_CUR, SEEK_END
,分别是文件头,当前位置,文件尾。
int ftell(FILE *stream)
,返回当前文件指针位置距离文件头的字节数。
通过上面两个函数可以获取一个文件的大小:
FILE *fp = fopen("in.txt", "r");
fseek(fp, SEEK_END);
int size = ftell(fp);
fseek(fp, SEEK_SET);
关闭文件
int fclose( FILE *fp );
时间函数
struct tm {
int tm_sec; /* 秒,范围从 0 到 59 */
int tm_min; /* 分,范围从 0 到 59 */
int tm_hour; /* 小时,范围从 0 到 23 */
int tm_mday; /* 一月中的第几天,范围从 1 到 31 */
int tm_mon; /* 月,范围从 0 到 11 */
int tm_year; /* 自 1900 年起的年数 */
int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
int tm_isdst; /* 夏令时 */
};
time_t time(time_t *t)
,返回自元时间以来经过的秒数,该秒数会同时返回给参数和返回值。struct tm *localtime(const time_t *t)
,返回当地时区时间。struct tm *gmtime(const time_t *t)
,返回用标准格林尼治时区(GMT)表示。time_t mktime(stuct tm *time)
,将结构体时间转化为秒数。double difftime(time_t t2, time_t t1)
,返回 t2-t1 的差值。size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)
,根据 format 中定义的格式化规则,格式化结构 timeptr 表示的时间,并把它存储在 str 中。clock_t clock(void)
,返回处理器时钟所以用时间,用于得到程序使用时间。
其他
命令行参数
// ./a.ot hahaha
int main(int argc, char *argv[])
{
printf("可执行程序 %s,参数个数为[%d],运行输出:[%s]\n", argv[0], argc, argv[1]);
return 0;
}
控制台动态刷新结果
printf("\r%d", count++);
// printf("\b\b%d", count++);
CPP 的基础
定义常量
#define Pi 3.141592654
const double PI = 3.1415926;
CPP 关键字
bool, namespace, using, class, private, public, protected, this, super, template, new, delete, inline, virtual, true, false
引用类型
引用变量是一个别名,引用是一种安全指针的机制。T & a = b;
int a = 5;
int &b = a; // b为a的引用
int swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
int a = 1, b= 2;
swap(a, b);
形参的默认值
在 CPP 中,函数的形参可以指定默认值,并且拥有默认值的参数必须放在最后面。
若定义的函数有前向引用申明,那么默认参数值必须在声明中定义,在实现的时候不可以再次指定默认值。
函数的重载
函数名相同,函数的参数个数或者参数类型不同,则称之为函数重载。
函数的返回值类型不可以作为判断函数重载的依据。
内联函数
定义函数时在返回类型前面添加关键字inline
,在编译时会用在调用函数的位置展开函数体,以减少参数传递,控制转移等的开销。
内联函数体内不能有循环和 switch 等语句,函数体必须简单。
异常处理
try {
// 保护代码
}catch( ExceptionName e1 ) {
// catch 块
cout << e1.what() << endl;
}catch( ExceptionName e2 ) {
// catch 块
}catch( ExceptionName eN ) {
// catch 块
}catch( ... ){
// 处理任何异常
}
try {
if (str == "int") throw 100;
else throw "ahhaha";
}catch(const int a) {
cout << a << endl;
}catch(const string b) {
cout << b << endl;
}catch( ... ) {
cout << "other exception" << endl;
}
多线程
#include <iostream>
#include <thread>
using namespace std;
void fun(int num)
{
while(true){
cout << num << endl;
}
}
int main()
{
thread t1(fun, 1);
thread t2(fun, 2);
// t1.join(),主线程会阻塞在这里
// t1.detach(),线程会后台运行,无需阻塞等待
// 主线程不会等待子线程结束
t1.join();
t2.join();
}