C 与 C++相关内容,
一部分从网上摘录。
个人能力水平有限,
不能保证没有错误。

——王心意,2018.11.13

必知

1
2
【必知】整数/整数 = 整数,向下取整。
3/5 == 0, 需要改成:3.0/5 == 0.6
1
a = 10, a += a *= 4; // 40 + 40 = 80
1
2
3
a == b < c;  // a == (b < c) // 判断
a = b < c; // a = (b < c) // 0 或 1
b < c == a; // 0 或 1 == a
1
for (; ; i++) { /*...*/} 中的 break 不会执行 i++ 语句,continue 会执行 i++
1
字符串复制:while (*p1++ = *p2++); // 谭浩强的书上有
1
2
*p++;     /* 地址++ */
(*p)++; /* 值++ */

读入回车解决方案

1
2
3
4
5
getchar(); // 会读入上一个scanf的回车
scanf("%c", &c); // 这是想要的字符

while(scanf(" %c", &c) != EOF); // 避免读取回车
while(scanf("%c%*c", &c) != EOF); // 强行去掉回车

计算机的浮点值计算有个”不确定尾数“的现象:

1
2
0.1+0.2 != 0.3 // 0.30000000000000004
0.1+0.5 == 0.6

原因在 https://0.30000000000000004.com

1
2
3
char a[] = "hello"; strcpy(a, "hehe"); // 正确,字符串常量能当做临时的指针常量

char *p = "hello"; p[2] = 'a'; // 错误,常量不能改
1
2
3
int a[5], (*p)[5]; // 指向数组的指针,下标必须明确
p = &a;
(*p)[i] == a[i];
1
puts(str[2])、strlen(&str[2])、gets(str+2); //都是从第二个位置开始。输入后末尾自动加上 '\0' (即 ASCII码的 0)
1
2
int a = 3; int &b = a; // 引用类型:对b操作就是对a操作,printf("%d", b)就是3
void swap( int &a, int &b); //引用,比指针方便

int 或者 long long 变量的某个数字,如:125 / 10 % 10; //倒数第二位

函数 floor( f ) 对浮点数取整,但是因为浮点数误差,一般用:floor( f + 0.5);
浮点数陷阱:for (double i = 0; i != 10; i++); // 死循环

升级

1
printf("%d %d %d", a, a++, a++); //在有些编译器上(实测如VC,不包括VS),从右到左执行,从左到右输出,a=10时会输出:12, 11, 10
1
a = b = 3; // 是从右到左开始运算,而非从左到右

不使用临时变量交换两个整数

1
2
3
a = a + b
b = a - b
a = a - b

位运算:

1
2
3
a = a ^ b;
b = a ^ b;
a = a ^ b;

输出”\r”字符串,光标位置回调到行首,并逐字覆盖这一行的文字(可用于做表格)

1
int i1, i2; // 分配空间(例子):i1:10008,i2:10004  逆序添加

static int m; 可以让其他文件不能调用变量或者函数,作用域只限于本模块
头文件不要定义全局变量(多次调用会报告已声明),可以用 extern

很大的数组比如几百亿的,开到全局(main函数外面,太大了也必须全局),会全部自动初始化成 0

(2^32) - 1 = 4294967295unsigned 的大小
-n 的内部表示是:(2 ^ 32) - n;

判断素数:

1
2
3
4
if (n < 2) return false;
for (int i = 2; i * i <= n; i++)
if (n % i == 0) return false;
return true;

也可以素数筛按照倍数打表(多次用到或数字很大时)

int q = 0; !q 的值在 0 和 非零 之间变换

1
scanf("%*s"); // 可以跳过输入一个字符串,其他类型同理
1
2
3
char t[] = "hello\0world";
sizeof(t) == 12;
strlen(t) == 5;

数字转换成字符串:sprintf(str, "%d", num);
字符串转换成数字:sscanf(str, "%d", &num);

清空queue等STL的数据:que.swap( queue<T>() ) ; //消亡值语义

void指针(仅用于代表地址):

1
2
3
4
5
6
7
8
9
10
void zero(void * data, int bytesize)
{
char * p = (char *)data;
for (int i = 0; i < bytesize; i++)
*ptr++ = 0;
}

int nums[30]; zero(nums, sizeof(nums)); //不管什么类型,都能按照字符(字节)来清零,因为字节总长度不变

Person p1; zero(p1, sizeof(p1)); //Person 为 struct 结构体,也能用同一个函数来清零 或者 复制数据

strncatstrncpy:比较安全的函数,第三个参数是数量,比如:sizeof(num)/sizeof(int)

指向函数的指针:

1
2
3
typedef void (* intFunc)(int i);
test1(int a) { printf("%d\n", a); } // intFunc 就是一个指向函数test1的指针了
intFunc func = test1; func(3);
1
( a * 2 + 1 ) == ( a << 1 | 1 ); // 位运算速度快很多很多
1
std::ios::sync_with_stdio(false);  // 禁用 cin 和 stdin 的同步,大大加快C艹的cin速度,与scanf相差无几(约为三倍)
1
a / b % m ==  a % (m * b) / b // 取模

字符串常量[下标],例如:"qwerty"[4] == 'r'

C语言只有一维数组,但数组元素可以是另一个数组,仿真出多维数组

16进制用0x开头,八进制用0开头;小数常量默认double,末尾可加f表示为float;整数常量默认int,末尾可加l表示是long;其他格式同理(然而没必要的,有隐式转换)

随机数(例如rand()函数)并不是真的随机(但有些新设备可以实现真随机)

叨唠

// C语言的爸爸是B语言没错

// 即使从面向过程的C语言转到面向对象的C++,你也不一定能找到对象。

// 计算机的减法乘法都是通过加法器来实现的(最终都可以分解为与或非逻辑门组合运算)

// 大部分程序员不用关注二进制是啥(但考试要考到)

// 你写程序的大部分时间不是在写代码

// 看到这句话将会很开心: 0 error,0 warning

// 项目中适当的注释往往比代码本身还重要

// 仅用顺序结构,分支结构和循环结果能写出所有的可计算函数。

// 可以用多个不同的编辑器看看这段注释的颜色:/*/zhushi/*/


偏僻知识点

来自网络

无符号int自动转换

1
2
3
4
5
6
7
8
9
10
void foo()
{
unsigned int a = 6;
int b = -20;
(a+b > 6)?puts("> 6") : puts("<= 6"); //输出 >6
/*
原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型,因此-20变成了一个非常大的
正整数,所以该表达式计算出的结果大于 6
*/
}

数组共同体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union
{
int i;
char x[2];
}a;
void fun()
{
a.x[0] = 10;
a.x[1] = 2;
printf("%d-%d-%d\n",a.i,a.x[0],a.x[1]);//522-10-2 :说明a.x[0]与a.x[1]互无影响,所以它们的地址是不同的。a.i=a.x[0]+a.x[1]*256(2的8次方);
a.i=1027;
printf("%d-%d-%d\n",a.i,a.x[0],a.x[1]);//1027-3-4 1027=(4)*256+(3)
a.x[0] = 300;
a.x[1] = 10;
printf("%d-%d-%d\n",a.i,a.x[0],a.x[1]);//2604-44-10, a.x[0]=300-256=44;a.i=a.x[0]+a.x[1]*256;
}

union这种类型,就是内部的变量共同使用一块空间,按照size大的分配,int i 占4个字节,char x[2] 占2个,所以一共分配了4个字节。
一共4个字节的内存,对应x来说相当于占用了低2个字节,而给x赋值的1,和10,就存在个位和十位上了(十六进制)

公用体公用一个内存区域sizeof(a)为共用体成员中长度最长的成员的size。即i

1
2
3
4
int:             (|_|_|_|_|_|_|_|_|)(|_|_|_|_|_|_|_|_|)(|_|_|_|_|_|_|_|_|)(|_|_|_|_|_|_|_|_|)
char x[2] : (|_|_|_|_|_|_|_|_|)(|_|_|_|_|_|_|_|_|)
^ ^
高地址 低地址

分析:

1
2
a.x[0] = 10  ============>(|0|0|0|0|1|0|1|0|)
a.x[1] = 2 ============>(|0|0|0|0|0|0|1|0|)

公用体公用sizeof(int)长度即4字节32为,则赋值后共用体内存为

1
a:               (|0|0|0|0|0|0|0|0|)(|0|0|0|0|0|0|0|0|)(|0|0|0|0|0|0|1|0|)(|0|0|0|0|1|0|1|0|)

a.i 为4字节整型,则 i = 2^9 + 2^3 + 2^1 = 256 + 8 + 2 = 522

宏定义运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
#define SQR(X) X*X
int main(void)
{
int i=10,j=5,n=10;
n*=i+j;//n=n*(i+j)
cout<<n<<endl;//150

int a = 10,k = 2, m=1;
a /= SQR(k+m)/SQR(k+m); // 等效于 a /= (k+m*k+m/k+m*k+m)
cout<<a<<endl;//1
return 0;
}

宏定义运算尽量添加小括号

1
#define SQR(X) (X*X)

sizeof(union)

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
union A {int a_int1;double a_double;int a_int2;};
typedef struct{A a1;char y;} B;
class C{double c_double;B b1;A a2;};
int main(void)
{
cout<<sizeof(A)<<endl;//8
cout<<sizeof(B)<<endl;//16
cout<<sizeof(C)<<endl;//32
return 0;
}

数组内存分配

1
2
3
4
5
6
7
8
9
10
11
12
char str1[] = "abc";
char str2[] = "abc";
const char str3[] = "abc";
const char str4[] = "abc";
const char *str5 = "abc";
const char *str6 = "abc";
char *str7 = "abc";
char *str8 = "abc";
cout << ( str1 == str2 ) << endl; // 0
cout << ( str3 == str4 ) << endl; // 0
cout << ( str5 == str6 ) << endl; // 1
cout << ( str7 == str8 ) << endl; // 1

解答:str1,str2,str3,str4是数组变量,它们有各自的内存空间;
而str5,str6,str7,str8是指针,它们指向相同的常量区域。

char <==> int

应该不算偏僻,ACM刷题经常会用到(刷题之外就用到的不多了)

1
2
3
4
5
6
//一位整数 to  char
char x=5+'0'; //'5'
//一位整数的char to int
int y='5'-'0'; //5
//获取'a'后面的'd'
char z='a'+3;//'d'

自增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
int a = 4;
a += (a++);//==>> a=a+(a++);
cout<<a<<endl;//9
}
{
int a = 4;
a += (++a);//==>> a = a+(++a);
cout<<a<<endl;//10
}
{
int a = 4;
//(a++) += a;//error,"+="左操作数必须为左值
cout<<a<<endl;
}
{
int a = 4;
(++a) += (a++);
//A=(++a);
//A=A+(a++)
cout<<a<<endl;//11
}

总结:(a++),在整个表达式执行完后,才+1;
(++a),在整个表达式执行前就+1;

连接下一行

行尾添加反斜杠 \

如果在宏定义中使用,则真正代码也会包括换行。可用来做接口宏。

多行字符串也能用这个来拼接。

数组下标为0

数组下标为0时,指针p不分配空间

1
2
3
4
5
Struct MutableLenArray 
{
int count;
char p[0];
};

任意字符串

使用 # 符号

1
2
#define TO_STRING(x) #x
const char *str = TO_STRING( test );

str的内容就是" test ",也就是说#会把其后的符号 直接加上双引号。

符号连接

##符号会连接两个符号,从而产生新的符号(词法层次),例如:

1
2
#define SIGN( x ) INT_##x
int SIGN( 1 );

宏被展开后将成为:int INT_1;

变参宏

1
2
#define LOG( format, ... ) printf( format, VA_ARGS )
LOG( "%s %d", str, count );

VA_ARGS是系统预定义宏,被自动替换为参数列表。

宏例外

当一个宏参数被放进宏体时,如果宏体(外部的宏)参数有#,则宏体的参数不会展开。若要展开,则需要借助第三个宏

1
2
3
#define PARAM(x) PARAM1(x)
#define PARAM1( x ) #x
#define ADDPARAM( x ) INT_##x

函数参数指针与引用

函数参数指针和引用的区别:引用必须已经初始化,且不能为空;指针可以

const修饰指针

1
2
3
4
5
6
7
8
int a = 1;
// const在*左侧,修饰指针指向的变量,表示指针指向的是常量,即(*a)不允许改变
const int *b = &a;
int const *c = &a;
// const在*右侧,修饰指针,表示指针指向的位置不能改变,且必须定义时初始化
int* const d = &a;
// 均为常量
const int* const e = &a;

const引用常量

const 引用能引用常量:const double &PI = 3.14

非const不能引用常量

const函数

int fun() const; 只读函数,不允许修改其中成员变量

const int *fun(); 修饰的是返回值,返回常量(这里是指向常量的指针)

const对象只能调用const函数;const成员函数中无法调用非const成员变量,也无法修改const成员变量

如果在常函数中真的需要修改某个成员变量的数据,那么需要这个成员被 mutable修饰

C与C++的bool

  1. C++具有真的布尔类型,bool是C++中的关键字,在C语言中使用布尔类型需要导入头文件stdbool.h(在C11中bool应该是数据类型了)。
  2. 在C++中 true false 是关键字,而在C语言中不是。
  3. 在C++中 true false 是1字节,而C语言中是4字节。

C++的void*

  1. C语言中void* 可以与任意类型指针 自动转换。
  2. C++中void*不能给其他类型的指针直接赋值,必须强制类型转换,但其他类型的指针可以自动给void*赋值。
  3. C++为什么这样修改void*
    为了更安全,所以C++类型检查更严格。

操作符别名

某些特殊语言的键没有~,&符号,国际标准化组织为一些操作符规定了别名,以便使用这些语言的键盘也能输入正确的C/C++代码。 C95和C++98以后的语言标准都支持ISO-646

1
2
3
4
5
6
and	&&
or ||
not !
{ <%
} %>
# :%

确定函数的步骤

  1. 候选函数
    函数调用的第一步就是确定所有可调用的函数的集合(函数名、作用域),该集合中的函数就是候选函数。
  2. 选择可行函数
    从候选函数中选择一个或多个函数,选择的标准是参数个数相同,而且通过类型提升实参可被隐式转换为形参。
  3. 寻找最佳匹配
    优先每个参数都完全匹配的方案,其次参数完全匹配的个数,再其次是浪费内存的字节数。

new/delete和malloc/free的区别?

不同点:

方面 new/delete malloc/free
身份 运算符 标准库函数
参数 类型(自动计算) 字节数(手动计算)
返回值 带类型的地址 void*地址
调用构造 自动调用 不能调用构造/析构函数
出错 抛出异常 返回NULL

相同点:

  1. 都能管理堆内存

  2. 不能重复释放

  3. 可以释放NULL

    当分配的内存过大,没有能满足需求的整块内存就会抛出异常 std::bad_alloc

new/delete/new[]/delete[]运算符重载

  1. C++缺省的堆内存管理器速度较慢,重载new/delete底层使用malloc/free可以提高运行速度
  2. new在失败会产生异常,而每次使用new时为了安全都应该进行异常捕获,而重载new操作符只需要在操作符函数中进行一次错误处理即可。
  3. 在一些占字节数比较小的类,频繁使用new,可能会产生大量的内存碎片,而重载new操作符后,可以适当的扩大每次申请的字节数,减少内存碎片产生的机率。
  4. 重载 new/delete 可以记录堆内存使用的信息
  5. 重载 delete 可以检查到释放内存失败时的信息,检查到内存泄漏。

类对象的创建过程与释放过程。

创建:分配内存(对象)-> 父类构造-> 成员构造-> 自己构造
父类构造:按照继承表从左到右依次构造。
成员构造:按照声明顺序从上至下依次构造。

释放:自己析构-> 成员析构-> 父类析构-> 释放内存(对象)
成员析构:按照声明顺序从下到上依次构造。
父类析构:按照继承表从右到左依次构造。

操作符重载

成员函数

1
2
constoperator#(const 类& that) const
{ return 类(参数#参数); }

全局函数(可以加友元)

1
constoperator#(const 类& a,const 类& b) {}

赋值类型时去掉左const

特殊操作符的重载

  1. 下标操作符[]

    1
    类型& operator[](int i){}
  2. 函数操作符()

    一个类如果重载函数操作符,那么它的对象就可以像函数一样使用,参数的个数、返回值类型,可以不确定,它是唯一一个可以参数有缺省参数的操作符

  3. 解引用操作符*、成员访问操作符->
    像指针一样使用(智能指针)

运算符重载的注意点

  1. 不能重载的操作符

    • 域限定符 ::
    • 直接成员访问操作符 .
    • 三目操作符 ?:
    • 字节长度操作符 sizeof
    • 类型信息操作符 typeid
  2. 重载操作符不能修改操作符的优先级

  3. 无法重载所有基本类型的操作符运算

  4. 不能修改操作符的参数个数

  5. 不能发明新的操作符

钻石继承

一个子类继承多个父类,这些父类有一个共同的祖先,这种继承叫钻石继承。
钻石继承不会导致继承错误,但访问祖先类中的成员时每次需要使用 类名::成员名 ,重点是这种继承会造成冗余。

虚继承

当进行钻石继承时,祖先类中的内容会有冗余,而进行虚继承后,在子类中的内容只会保留一份。
注意:但使用虚继承时,子类中会多了一些内容(指向从祖先类继承来的成员)。

类型信息typeid

用于获取数据的类型信息,同时还支持 == != 用来比较是否是同一种类型,或者是否具有继承关系(但没法判断出实际new的对象类型)

1
if (typeid(a) == typeid(b))
1
2
3
Parent* p = new Child();
cout << (typeid(*p) == typeid(Child)) << endl;
cout << (typeid(p) == typeid(Child*)) << endl;

C语言主要关键词

基本类型:int, short,long, signed, unsigned,char,float, double,void

控制:if...else, switch...case...default, for , while, do...while,return, break, continue,goto

自定义类型:enum, struct, union, typedef

修饰词:const, static, extern, inline, restrict, volatile

运算符:+, -, *,/, %,++,--,&,| ,~, ^,&&,||,!,<,>,<=,>=,==,!=, <<,>>, ., ->,?:,sizeof及复合运算符

预处理器:#include, #define, #undef, #if/#ifdef/#ifndef...#elif...#else...#endif, defined, #pragma,#error

有极少数关键字有多种语义,例如staticvoid

掌握上面这些就算入门了。