循环嵌套
在多层循环中,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
如果循环体内存在逻辑判断,并且循环次数很大,应将逻辑判断移到循环体外。
引用
不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)
int &m=NULL; //error
不能建立指针的引用
int * &n = &m; //error
数组与指针
指向字符串的指针不能修改字符串(常量字符串位于静态储存区,不可修改!)。数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
char a[]="hello";
a[0] = 'x'; //正确
char *p = "world"; //p指向常量储存区 可理解为 const char *p = "world"
p[0] = 'x'; //错误,编译器不能发现该错误
当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针
void Func(char a[100])
{
cout<< sizeof(a) << endl; //4 字节而不是100字节
}
指针
如果 p 是 NULL 指针,那么 free 对 p 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 free 对 p连续操作两次就会导致程序运行错误。
*(ptr++) += 100; <-- 指针移一次
*(ptr++) = *(ptr++) + 100; <-- 指针移两次
库函数
1.整数转字符串:sprintf()
/*----------------------------------------------------------------------------
* 函数名:int sprintf( char *buffer, const char *format, [ argument] … );
* 功 能:把格式化的数据写入某个字符串缓冲区。
* 参 数:buffer:char型指针,指向将要写入的字符串的缓冲区。
format:格式化字符串。
[argument]...:可选参数,可以是任何类型的数据
* 返回值:返回写入buffer 的字符数,出错则返回-1. 如果 buffer 或 format 是空指
针,且不出错而继续,函数将返回-1,并且 errno 会被设置为 EINVAL
----------------------------------------------------------------------------*/
举例:
int n=123;
char str[4];
sprintf(str,"%d",n);
puts(str); //str*="123"
2.atof()函数在 <stdlib.h> 中
变量
extern
C语言中不能将变量放在.h文件中重复调用 可用extern
volatile
立即读取内存变量(禁止编译器优化)
volatile 影响编译器编译的结果,指出 volatile 变量每次使用时都需要去内存里重新读取它的值,而一旦修改此变量就直接写入内存,而不是操作cache中的变量。 volatile 变量有关的运算,不要进行特殊优化。
{
int I = 10;
int j = I;
int k=I;
}
对于上面的代码段,优化的作法是,编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会从 cache 中把上次读的数据 i 放在 k 中。而不是重 新从 i 的内存里面读。这样以来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问,不会出错。
另一个例子:
[task1.c]
int volatile i;
while(i)
{
do_something();
}
[task2.c]
do_something(i);
如果 i 没有被 volatie 修饰,当 while 循环执行时,另一段程序并发的执行了 i=0 ,这个循环仍不会退出,因为每次循环都是检查cache中的值。 如果有 volatie 修饰,那么循环结束,因为循环每次检查 i 的时候,会先从内存把 i 读入寄存器,这个时候 i 在其它地方被赋 0 ,则循环结束。
对于程序中存在多个执行流程访问同一全局变量的情况, volatile 限定符是必要的。
register
在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的该单元,影响程序的执行效率。因此,C\C++语言还定义了一种变量,不是保存在内存上,而是直接存储在CPU中的寄存器中,这种变量称为寄存器变量。
寄存器是与机器硬件密切相关的,不同类型的计算机,寄存器的数目是不一样的,通常为2到3个,对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为自动变量。
转义字符 \t
\t代表是一个tab键值,就是8位,具体空格的个数,跟你的数值有关系,比如你的字符串是abc,加个\t,则空格是5个,如果字符串是abcde,加\t,则空格是3个
位段(bit field)
该种形式出现于结构体或共用体的定义中,是位域定义的标准形式。其使用方式为:
struct name
{
type var_name : n;
};
含义为,在结构体name汇总,成员变量var_name占用空间为n位。n为正整数,其值必须小于type类型占用的位数。比如type如果是int,占4字节32位,那么n必须是1~31之间的整数。
对于位域类型的成员,在赋值时如果实际值超过n位所能表达的范围,那么超出部分将会被截掉,只保存低位值。如int var:4,本身只有4位的空间,如果赋值var = 20, 由于20的二进制值为10100,实际为五位,这时var实际被赋值的就是低四位,0100,即4。
由于C语言中的地址是针对字节计算的,所以位域类型的成员变量不支持取地址操作,即对于结构体a, 如果存在位域成员变量var,那么&a.var是非法的,编译会出错。
__packed
__packed是字节对齐的意思,比如说int float double char它的总大小是4 + 4 + 8 + 1 = 17。但如果不用__packed的话,系统将以默认的方式对齐(假设是4字节),那么它占4 + 4 + 8 + 4 = 20;(不足4字节以4字节补齐)。
注:在VS2008上述方法不能用了,如果想设置对齐方式,只要选择工程属性-配置属性-C/C++-代码生成就能设置,它的选项有1、2、4、8、16。在GCC下,可以在加上#pragma pack(4) 4字节对齐的意思,其它同理。
数据合并
u8 a[2] = {0x10,0x20};
u16 b;
b = a[1]<<8 + a[0]; //错误,必须加括号
b = (a[1]<<8) + a[0]; //正确(隐式类型转换)
b = ((u16)a[1]<<8) + (u16)a[0]; //正确(显式类型转换)
b = ((u16)a[1]<<8) | (u16)a[0]; //正确
数据拆分
u16 b = 0x1020;
u8 a[2];
a[0] = b/256; //a[0] = 0x10
a[1] = b%256; //a[1] = 0x20
a[0] = b/0xFF;
a[1] = b%0xFF;
a[0] = b>>8; //隐式转换
a[1] = b;
a[0] = u8(b>>8); //显式转换
a[1] = (u8)b;
C++几个概念
- 不同类型成员的访问权限
public: 内、外、派
protected: 内、派
private: 内
- 初始化列表
构造函数除了有参数列表之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
class X
{
private:
int id;
int age;
int other;
public:
X(int i, int a, int o): id(i), age(a) //初始化列表
{
other = o; //也可以使用传统初始化方法
}
};
- const 成员变量
即为 const 修饰的成员变量,其只能在构造函数中通过初始化列表来进行初始化,数据为 “只读”,不会再程序运行中发生改变(不允许赋值操作)。
- const 成员函数
指 const 放在成员函数的后面,表明这个函数不会修改类中任何数据,也不能在 const 的成员函数中,调用其他非 const 的成员函数。
- 静态数据成员
即数据成员的定义前面增加了 static 关键字,其只能在外部程序开始执行前初始化一次(类似于全局变量那样创建),然后被所有成员共享,任一对象都可修改该共享数据。主要用于各个对象的公用数据,如统计总数(如下代码中的 i )、各种文件的共用路径等(如下代码中的 path 和 p)。静态数据成员独立于所有对象,甚至在对象未被声明时已经存在(编译时已被创建并初始化,默认为0),在main函数(程序)结束前,其内存都不会销毁。
class Student{
public:
//不能在此处初始化,声明类时不分配内存
static int count;
static char univ[];
static const char *addr;
Student() { count++; } //每定义一个学生,人数自动加一
};
//一般在类之后,main之前进行定义和初始化(带上数据类型)
int Student::count = 0;
char Student::univ[20] = "MIT";
const char * Student::addr = "Boston";
int main()
{
Student::count = 10; //在声明对象之前已经存在
// base::path[20] = "/home/alise"; //错误
strcpy(Student::univ, "Harvard"); //原来的字符串已被覆盖
memcpy(Student::univ, "Stanford", 9);
Student::addr = "San Francisco"; //正确,p指向了另一个字符串
Student jack, tom; //创建两个对象后count变为12
cout<< jack.count <<endl; //输出 12
cout<< tom.count <<endl; //输出 12
jack.count = 2; //所有对象的 count 都会改变
tom.addr = "xian";
cout<< jack.count <<endl; //输出 2
cout<< tom.count <<endl; //输出 2
cout<< jack.univ <<endl; //输出 "Stanford"
cout<< jack.addr <<endl; //输出 "xian"
cout<< tom.addr <<endl; //输出 "xian"
return 0;
}
【引申】:数组、指针与字符串
对数组初始化字符串时,数组仅表示这个字符串的首地址(不是变量),故其不能被赋值(只能在初始化时定义一次,即定义了表示的内存地址及内存大小),数组只能表示该字符串的首地址。其字符串分配在 “栈区”,故可以通过数组修改字符串,想要一次全部修改时,可通过 strcpy() 或memcpy() 函数。
对指针初始化字符串时,指针变量储存在 “栈区”,而字符串储存在 “常量储存区”,故不可以通过指针修改字符串(所以一般定义时要加上 const 关键字修饰),但指针可以重新赋值(重新指向另一个字符串)。
- 静态成员函数
即成员函数前面加 static,其只能访问静态数据成员,不能访问非静态数据成员,也不能访问非静态成员函数。
class Student{
private:
static int count;
public:
static void set(int n) { count = n; }
// static void set(int count) {count = count; } //错误
static void show(void) { cout<< count <<endl; }
};
int Student::count = 0; //可外部初始化
int main()
{
// Student::count = 1; //错误,私有数据成员,不能直接调用
Student::set(1); //通过类名调用静态成员函数
Student::show();
Student tom;
tom.show(); //输出 1
tom.set(2);
tom.show(); //输出 2
return 0;
}
【注意】:静态成员函数的形参不能与静态数据成员同名,否则参数传递会失效。
- 友元
指在函数或类的前面加 friend 关键字,包括:一般友元函数、友元成员函数、友元类。其可允许在类的外部访问类的私有成员。
:: 运算符
:: 是运算符中等级最高的,它分为三种:
(1) global scope(全局作用域符),用法(::name)
int a;
int main()
{
int a;
a=1; // 局部变量a
::a=2; // 全局变量a
}
当全局变量在局部函数中与其中某个变量重名,那么就可以用::来区分
(2)类的外部定义成员函数
(3)调用静态数据成员和静态成员函数
(4) class scope(类作用域符),用法(class::name)
例如:C继承A,B,而A,B中皆有成员 成员函数f(),则当从C中调用 f 时会产生二义性
C.f(); // 二义性错误
C.A::f(); // 正确,调用基类A中的f()
C.B::f(); // 正确,调用基类B中的f()
(5)引用类中 typedef 的新类型
C++引入了 “仅在类内部起作用的类型别名(并且还有派生方式之分)” ,通过限制该类型别名的作用域来防止冲突。
比如同样表示长度,可能有的类中只须char即可,有的类中要用int,而有的类可能连long都嫌小。
class A{
private:
typedef char size_c;
public:
typedef int size_mm;
size_mm length;
size_c *str;
size_mm set(size_mm l)
{
length = l;
return length;
}
};
class B{
public:
typedef int size_cm;
size_cm length;
};
int main()
{
// A::size_c *path; //错误,私有类型不能在外部引用
A::size_mm a; //定义类A的size_mm类型变量
return 0;
}
(6) namespace scope(命名空间作用域符),用法(namespace::name)
虚基类
为了解决“二义性”的问题
假设有基类A,如果直接让类 B,C 继承 A,而 D 继承 B 和 C,即一种 “菱形” 结构,当D访问A中的成员时,则会产生 “二义性” 的问题,不知是通过 B 这条路的 A 还是 C 这条路径的A。
故可以将B和C定义为虚继承,此时B和C共享A,当D访问A中的成员时就没有了“二义性”(只有一个A)。
派生对象指针
- 指向基类对象的指针可以指向其公有派生的对象,且只能访问派生对象中从基类继承而来的成员。
- 指向派生对象的指针不能指向其基类对象
虚函数
虚函数允许函数调用在运行时才建立,
如:当A中有成员函数 f(),B继承A,其也有成员函数 f(),当定义基类A的指针p后,再令其指向派生B的对象,调用 p->f(),此时调用的函数为A中的 f()。
这时,将基类A中的成员函数 f() 声明为虚函数(B中的 f() 可以不声明为虚函数),同上调用 p->f() 后,调用的将是B中的 f()。
即,虚函数机制允许指向基类的指针,指谁,运行谁的(同名)成员函数。
纯虚函数 / 抽象类
纯虚函数:在基类中没有定义,但要求在派生类中定义自己的版本。
抽象类:至少包含一个纯虚函数的类。
如:计算三角形和矩形的面积,两者都可以提炼出 “形状” 这个基类,其包含长,高和计算面积的纯虚函数(仅声明,没有定义),而在派生类 “三角形” 和 “矩形” 则继承 “形状” 类,并分别在其中定义具体的计算面积函数。
参考:俄罗斯方块代码(链接)
内存分配
C/C++语言中分了如下几个内存区域
1. 栈区
是最常见的内存区域,由编译器自动分配并在函数结束时释放,如函数的形参、实参和局部变量。运行效率高,但容量有限。其缓存方式为 “后进先出”,使用的是一级缓存。
【注】:在Windows下,栈是向低地址扩展的数据结构(Linux根据版本,情况不同),是一块连续的内存的区域,即栈顶的地址和栈的最大容量是系统预先规定好的(在VC6下面,默认的栈空间大小是1M,多了会溢出,运行出错)
2. 堆区
分配(malloc() / new)和释放(free() / delete)皆由程序员管理(程序全部执行完后也会由OS释放),可在运行时任意调整大小(故也称为“动态内存分配”)可使用的容量也及较大,使用的是二级缓存。
【注】:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。
3. 静态储存区
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量和 static 变量。
4. 常量区
存放常量,不可修改,如宏定义、代码中的数字、常量字符串、C++中 const 修饰的变量。
【注】:C中 const 修饰的变量仍在 “栈” 中。
5. 程序代码区
存放程序文件,运行后不可修改
一个例子:
// 整个程序文件在程序代码区
#include<iostream>
#define PI 3.14 // 常量区
int a = PI; // PI在常量区,a在静态储存区
int main()
{
int b = 2; // 2在常量区,b在栈区
char s[] = "123abc"; // 数组s在栈中,内容为"123abc"
char *p1 = "456def"; // 指针 p1 在栈中,指向的字符串"456def"在常量区
int *p2 = (int *)malloc(sizeof(int)*10); // 指针 p2 在栈中,指向的内存在堆中
static int d = 3; // 静态储存区
return 0;
}