一、#include "filename.h"和#include 的区别
#include "filename.h"是指编译器将从当前工作目录上开始查找此文件
#include 是指编译器将从标准库目录中开始查找此文件
二、头文件的作用
加强安全检测
通过头文件可能方便地调用库功能,而不必关心其实现方式
三、* , &修饰符的位置
对于*和&修饰符,为了避免误解,最好将修饰符紧靠变量名
四、if语句
不要将布尔变量与任何值进行比较,那会很容易出错的。
整形变量必须要有类型相同的值进行比较
浮点变量最好少比点,就算要比也要有值进行限制
指针变量要和NULL进行比较,不要和布尔型和整形比较
五、const和#define的比较
const有数据类型,#define没有数据类型
个别编译器中const可以进行调试,#define不可以进行调试
在类中定义常量有两种方式
1、 在类在声明常量,但不赋值,在构造函数初始化表中进行赋值;
2、 用枚举代替const常量。
六、C++函数中值的传递方式
有三种方式:值传递(Pass by value)、指针传递(Pass by pointer)、引用传递(Pass by reference)
void fun(char c) //pass by value
void fun(char *str) //pass by pointer
void fun(char &str) //pass by reference
如果输入参数是以值传递的话,最好使用引用传递代替,因为引用传递省去了临时对象的构造和析构
函数的类型不能省略,就算没有也要加个void
七、函数体中的指针或引用常量不能被返回
Char *func(void)
{
char str[]="Hello Word";
//这个是不能被返回的,因为str是个指定变量,不是一般的值,函数结束后会被注销掉
return str;
}
函数体内的指针变量并不会随着函数的消亡而自动释放
八、一个内存拷贝函数的实现体
void *memcpy(void *pvTo,const void *pvFrom,size_t size)
{
assert((pvTo!=NULL)&&(pvFrom!=NULL));
byte *pbTo=(byte*)pvTo; //防止地址被改变
byte *pbFrom=(byte*)pvFrom;
while (size-- >0)
pbTo++ = pbForm++;
return pvTo;
}
九、内存的分配方式
分配方式有三种,请记住,说不定那天去面试的时候就会有人问你这问题
1、 静态存储区,是在程序编译时就已经分配好的,在整个运行期间都存在,如全局变量、常量。
2、 栈上分配,函数内的局部变量就是从这分配的,但分配的内存容易有限。
3、 堆上分配,也称动态分配,如我们用new,malloc分配内存,用delete,free来释放的内存。
十、内存分配的注意事项
用new或malloc分配内存时,必须要对此指针赋初值。
用delete 或free释放内存后,必须要将指针指向NULL
不能修改指向常量的指针数据
十一、内容复制与比较
//数组......
char a[]="Hello Word!";
char b[10];
strcpy(b,a);
if (strcmp(a,b)==0)
{}
//指针......
char a[]="Hello Word!";
char *p;
p=new char[strlen(a)+1];
strcpy(p,a);
if (strcmp(p,a)==0)
{}
十二、sizeof的问题
记住一点,C++无法知道指针所指对象的大小,指针的大小永远为4字节
char a[]="Hello World!"
char *p=a;
count<
count<
而且,在函数中,数组参数退化为指针,所以下面的内容永远输出为4
void fun(char a[1000])
{
count<
}
十三、关于指针
1、 指针创建时必须被初始化
2、 指针在free 或delete后必须置为NULL
3、 指针的长度都为4字节
4、释放内存时,如果是数组指针,必须要释放掉所有的内存,如
char *p=new char[100];
strcpy(p,"Hello World");
delete []p; //注意前面的[]号
p=NULL;
5、数组指针的内容不能超过数组指针的最大容易。
如:
char *p=new char[5];
strcpy(p,"Hello World"); //报错 目标容易不够大
delete []p; //注意前面的[]号
p=NULL;
十四、关于malloc/free 和new /delete
l malloc/free 是C/C+的内存分配符,new /delete是C++的内存分配符。
l 注意:malloc/free是库函数,new/delete是运算符
l malloc/free不能执行构造函数与析构函数,而new/delete可以
l new/delete不能在C上运行,所以malloc/free不能被淘汰
l 两者都必须要成对使用
l C++中可以使用_set_new_hander函数来定义内存分配异常的处理
十五、C++的特性
C++新增加有重载(overload),内联(inline),Const,Virtual四种机制
重载和内联:即可用于全局函数,也可用于类的成员函数;
Const和Virtual:只可用于类的成员函数;
重载:在同一类中,函数名相同的函数。由不同的参数决定调用那个函数。函数可要不可要Virtual关键字。和全局函数同名的函数不叫重载。如果在类中调用同名的全局函数,必须用全局引用符号::引用。
覆盖是指派生类函数覆盖基类函数
函数名相同;
参数相同;
基类函数必须有Virtual关键字;
不同的范围(派生类和基类)。
隐藏是指派生类屏蔽了基类的同名函数相同
1、 函数名相同,但参数不同,此时不论基类有无Virtual关键字,基类函数将被隐藏。
2、 函数名相同,参数也相同,但基类无Virtual关键字(有就是覆盖),基类函数将被隐藏。
内联:inline关键字必须与定义体放在一起,而不是单单放在声明中。
Const:const是constant的缩写,"恒定不变"的意思。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1、 参数做输入用的指针型参数,加上const可防止被意外改动。
2、 按值引用的用户类型做输入参数时,最好将按值传递的改为引用传递,并加上const关键字,目的是为了提高效率。数据类型为内部类型的就没必要做这件事情;如:
将void Func(A a) 改为void Func(const A &a)。
而void func(int a)就没必要改成void func(const int &a);
3、 给返回值为指针类型的函数加上const,会使函数返回值不能被修改,赋给的变量也只能是const型变量。如:函数const char*GetString(void); char *str=GetString()将会出错。而const char *str=GetString()将是正确的。
4、 Const成员函数是指此函数体内只能调用Const成员变量,提高程序的键壮性。如声明函数 int GetCount(void) const;此函数体内就只能调用Const成员变量。
Virtual:虚函数:派生类可以覆盖掉的函数,纯虚函数:只是个空函数,没有函数实现体;
十六、extern"C"有什么作用?
Extern "C"是由C++提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数,加上extren "c"后,C++就能直接调用C函数了。
Extern "C"主要使用正规DLL函数的引用和导出 和 在C++包含C函数或C头文件时使用。使用时在前面加上extern "c" 关键字即可。
十七、构造函数与析构函数
派生类的构造函数应在初始化表里调用基类的构造函数;
派生类和基类的析构函数应加Virtual关键字。
不要小看构造函数和析构函数,其实编起来还是不容易。
#include
class Base
{
public:
virtual ~Base() { cout<< "~Base" << endl ; }
};
class Derived : public Base
{
public:
virtual ~Derived() { cout<< "~Derived" << endl ; }
};
void main(void)
{
Base * pB = new Derived; // upcast
delete pB;
}
输出结果为:
~Derived
~Base
如果析构函数不为虚,那么输出结果为
~Base
十八、#IFNDEF/#DEFINE/#ENDIF有什么作用
仿止该头文件被重复引用
基本解释
1、指针的本质是一个与地址相关的复合类型,它的值是数据存放的位置(地址);数组的本质则是一系列的变量。
2、数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,它的特征是"可变",所以我们常用指针来操作动态内存。
3、当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
问题:指针与数组
听说char a[]与char *a是一致的,是不是这样呢?
答案与分析:
指针和数组存在着一些本质的区别。当然,在某种情况下,比如数组作为函数的参数进行传递时,由于该数组自动退化为同类型的指针,所以在函数内部,作为函数参数传递进来的指针与数组确实具有一定的一致性,但这只是一种比较特殊的情况而已,在本质上,两者是有区别的。请看以下的例子:char a[] = "Hi, pig!"; char *p = "Hi, pig!";
上述两个变量的内存布局分别如下:
数组a需要在内存中占用8个字节的空间,这段内存区通过名字a来标志。指针p则需要4个字节的空间来存放地址,这4个字节用名字p来标志。其中存放的地址几乎可以指向任何地方,也可以哪里都不指,即空指针。目前这个p指向某地连续的8个字节,即字符串"Hi, pig!"。
另外,例如:对于a[2]和p[2],二者都返回字符‘i',但是编译器产生的执行代码却不一样。对于a[2],执行代码是从a的位置开始,向后移动2两个字节,然后取出其中的字符。对于p[2],执行代码是从p的位置取出一个地址,在其上加2,然后取出对应内存中的字符。
问题:数组指针
为什么在有些时候我们需要定义指向数组而不是指向数组元素的指针?如何定义?
答案与分析:
使用指针,目的是用来保存某个元素的地址,从而来利用指针独有的优点,那么在元素需要是数组的情况下,就理所当然要用到指向数组的指针,比如在高维需要动态生成情况下的多维数组。
定义例子如下: int (*pElement)[2]。
下面是一个例子: int array[2][3] = {{1,2,3},{4,5,6}};
int (*pa)[3]; //定义一个指向数组的指针
pa = &array[0]; // '&'符号能够体现pa的含义,表示是指向数组的指针
printf ("%d", (*pa)[0]); //将打印array[0][0],即1
pa++; // 猜一猜,它指向谁?array[1]?对了!
printf ("%d", (*pa)[0]); // 将打印array[1][0],即4
上述这个例子充分说明了数组指针—一种指向整个数组的指针的定义和使用。
需要说明的是,按照我们在第四篇讨论过的,指针的步进是参照其所指对象的大小的,因此,pa++将整个向后移动一个数组的尺寸,而不是仅仅向后移动一个数组元素的尺寸。
问题:指针数组
有如下定义:struct UT_TEST_STRUCT *pTo[2][MAX_NUM];
请分析这个定义的意义,并尝试说明这样的定义可能有哪些好处?
答案与分析:
前面我们谈了数组指针,现在又提到了指针数组,两者形式很相似,那么,如何区分两者的定义呢?分析如下:
数组指针是:指向数组的指针,比如 int (*pA)[5]。
指针数组是:指针构成的数组,比如int *pA[5]。
至于上述指针数组的好处,大致有如下两个很普遍的原因:
a)、各个指针内容可以按需要动态生成,避免了空间浪费。
b)、各个指针呈数组形式排列,索引起来非常方便。
在实际编程中,选择使用指针数组大多都是想要获得如上两个好处。
问题:指向指针的指针
在做一个文本处理程序的时候,有这样一个问题:什么样的数据结构适合于按行存储文本?
答案与分析:
首先,我们来分析文本的特点,文本的主要特征是具有很强的动态性,一行文本的字符个数或多或少不确定,整个文本所拥有的文本行数也是不确定的。这样的特征决定了用固定的二维数组存放文本行必然限制多多,缺乏灵活性。这种场合,使用指向指针的指针有很大的优越性。
现实中我们尝试用动态二维数组(本质就是指向指针的指针)来解决此问题:
图示是一个指针数组。所谓动态性指横向(对应每行文本的字符个数)和纵向(对应整个文本的行数)两个方向都可以变化。
就横向而言,因为指针的灵活性,它可以指向随意大小的字符数组,实现了横向动态性。
就竖向而言,可以动态生成及扩展需要的指针数组的大小。
下面的代码演示了这种动态数组的用途:
// 用于从文件中读取以 '\0'结尾的字符串的函数
extern char *getline(FILE *pFile);
FILE *pFile;
char **ppText = NULL; // 二维动态数组指针
char *pCurrText = NULL; // 指向当前输入字符串的指针
ULONG ulCurrLines = 0;
ULONG ulAllocedLines = 0;
while (p = getline(pFile))
{
if (ulCurrLines >= ulAllocedLines)
{
// * 当前竖向空间已经不够了,通过realloc对其进行扩展。
ulAllocedLines += 50; // 每次扩展50行。
ppText = realloc (ppText, ulAllocedLines * (char *));
if (NULL == ppText)
{
return; // 内存分配失败,返回
}
}
ppText[ulCurrLines++] = p; // 横向"扩展",指向不定长字符串
}
问题:指针数组与数组指针与指向指针的指针
指针和数组分别有如下的特征:
指针:动态分配,初始空间小
数组:索引方便,初始空间大
下面使用高维数组来说明指针数组、数组指针、指向指针的指针各自的适合场合。
多维静态数组:各维均确定,适用于整体空间需求不大的场合,此结构可方便索引,例a[10][40]。
数组指针:低维确定,高维需要动态生成的场合,例a[x][40]。
指针数组:高维确定,低维需要动态生成的场合,例a[10][y]。
指向指针的指针:高、低维均需要动态生成的场合,例a[x][y]。
问题:数组名相关问题
假设有一个整数数组a,a和&a的区别是什么?
答案与分析:
a == &a == &a[0],数组名a不占用存储空间。需要引用数组(非字符串)首地址的地方,我一般使用&a[0],使用a容易和指针混淆,使用&a容易和非指针变量混淆。
区别在于二者的类型。对数组a的直接引用将产生一个指向数组第一个元素的指针,而&a的结果则产生一个指向全部数组的指针。例如:int a[2] = {1, 2};
int *p = 0;
p = a; /* p指向a[0]所在的地方 */
x = *p; /* x = a[0] = 1*/
p = &a; /* 编译器会提示你错误,*/
/*显示整数指针与整数数组指针不一样 */
问题:函数指针与指针函数
请问:如下定义是什么意思:int *pF1(); int (*pF2)();
答案与分析:
首先清楚它们的定义:
指针函数,返回一个指针的函数。
函数指针,指向一个函数的指针。
可知:
pF1是一个指针函数,它返回一个指向int型数据的指针。
pF2是一个函数指针,它指向一个参数为空的函数,这个函数返回一个整数。
1,预定义宏
__LINE__ 当前代码行的行号(等于读到目前为止新行的字符数)
__FILE__ 源文件的名字
__DATE__ 以"Mmm dd yyyy"的形式转换日期
__TIME__ 以"hh:mm:ss"的形式转换日期
__cplusplus 一个包含正式的C++标准的日期
2,如果代码是在C/C++混合编程环境中编写的,可以将新的C++代码与旧的C代码相链接。所需要做的所有事情就是通过extern "C"链接规范来告诉C++翻译器,不要"砍掉"与C组件相匹配的外部名字,例如:
extern "C" void f(); // f() 在C环境下被编译
C++标准库中的C部分知道是否该砍掉名字(在第一章已解释过),这取决于在什么模式下(C或C++)编译。如果仔细阅读标准C头文件会发现销售商使用__cplusplus宏通过一个extern "C"块根据以下模式条件隐藏了标准C声明:
#if defined(__cplusplus)
extern "C"
{
#endif
#if defined(__cplusplus)
}
#endif
3,#if defined(X) <==> #ifdef X
defined运算符比其右边的等价指令要灵活多,因为可以把许多测试联合起来构成一个表达式:
#if defined(__cplusplus) && !defined(DEBUG)
4,预处理运算符
# 字符串化
## 加标记
defined 符号表查询
#define trace(x,format) printf(#x "=%" #format "\n",x)
#define trace2(i) trace(x##i,d)
5,当宏必须作出选择时,好的做法是把它写成一个表达式而不是语句
void __assert(char *cond,char *fname,long lineno){
fprintf(stderr,"Assertiion failed:%s, file %s, line %ld\n",cond,fname,lineno);
abort();
}
#define assert(cond) \
if(!(cond)) __assert(#cond,__FILE__,__LINE__) // 不好
#define assert(cond) \
((cond)?(void)0:__assert(#cond,__FILE__,__LINE__) // 好
6,预处理器的宏替换功能显而易见为你提供了很大的灵活性。但要记住以下两点限制:
A,无论何时预处理器在其替代文本中遇到当前的宏,不管在进程中有多么深的嵌套,它都不做扩展,只是保持不变(否则进程将永远不终止!)。
B,如果被充分扩展的语句相似于一条预处理指令(例如扩展以后结果是一条#include指令),它没有被调用,而是逐字地留在程序文本中。
7,三字符运算符序列
??= #
??( [
??/ \
??) ]
??' ^
??< {
??! |
??> }
??- ~
8,新的C++双字符运算符和保留字
}
<: [
:> ]
%% #
Bitand &
And &&
Bitor |
Or ||
Xor ^
Compl ~
and_eq &=
or_eq |=
xor_eq ^=
Not !
not_eq !=
9,翻译阶段
标准C和C++定义了9个不同的翻译阶段。当然,实现没有必要在代码中分成9个独立的阶段,但是翻译的结果必须好像已经这样做了一样,这9个阶段是:
1,物理源字符被映射到源字符集中。其中包括三字符组合替换以及诸如把回车/换行映射到一个单独的MSDOS环境下的换行字符那样的东西。在C++程序中,任何不在基础源字符集中的字符都被它的通用字符名替换。
2,所有以反斜杠结束的行都和它们接下来的行合并,并且删去反斜杠。
3,源码被分析成预处理标记,并且注释被一个单独的空字符所替换,C++双字符被识别为标记。
4,调用预处理指令并且扩展宏,对于任何被包含的文件循环地重复步骤1到4。
5,源字符退出字符常量序列,普通字符名被映射成执行字符集成员(例如,'\a'将在ASCII环境下转换成7的一个字节值)。
6,相邻的字符串被连接。
7,传统的编译:词汇和语义分析,并翻译成汇编语言或机器码。
8,(只有C++)执行任何待解决的模板实例。
9,链接:解决外部引用,准备好程序映像以便执行。
预处理器由步骤1到4组成。
10,存储两个指针差的便捷方法是把这个差存储在ptrdiff_t中,它在stddef.h中定义,包含在中。
ptrdiff_t diff=p-q;
11,说明指针转换
#include
using namespace std;
void main(){
int i=7;
char *cp=(char *)&i;
cout<<"The integer at "<<&i<<" == "<
//分别打印每个字节的值
for(int n=0;n cout<<"The byte at "<<(void *)(cp+n)<<" == "<}
12,位域
#include
using namespace std;
struct Date{
unsigned day:5;
unsigned mon:4;
unsigned year:7;
};
void main(){
struct Date d;
d.day=2;
d.mon=8;
d.year=92;
cout<}
/
#include
using namespace std;
struct Date{
unsigned day:5;
unsigned mon:4;
unsigned year:7;
};
void main(){
unsigned short date,year=92,mon=8,day=2;
Date *dp=(Date *)&date;
dp->day=day;
dp->mon=mon;
dp->year=year;
cout<}
13,在面向对象操作中,指向一个对象的指针称为"句柄"。
14,普通指针
通常编写能接收指向任意类型参数的函数是很方便的。这是很有必要的,例如,用标准的库函数memcpy,能够从一个地址向另一个地址拷贝一块内存。你也可能想调用memcpy来拷贝自己创建的结构:
struct mystruct a,b;
...
memcpy(&a,&b,sizeof(struct mystruct));
为了操作任意类型的指针,memcpy把它头两个参数声明为void型指针。可以不需要强制类型转换将任何类型的指针赋予void*类型。也可以在C而不是C++中将void*赋予其他任何类型的指针。这里说明了void指针的memcpy函数的简洁实现:
void* memcpy(void* target, const void* source, size_t n){
char *targetp=(char *)target;
const char *sourcep=(const char *)source;
while(n--)
*targetp++=*sourcep++;
return target;
}
这个版本的memcpy必须把指向void的指针赋予指向char指针一,这样它就可以每次传递内存块的一个字节,并对这个字节中的数据进行拷贝。试图复引用一个void*是没有意义的,因为它的大小是未知的。
15,
strncpy(s,t,n)[n]='\0';
a[i]=*(a+i)=*(i+a)=i[a]
size_t n=sizeof a/sizeof a[0];
cout<<(void*)"hello"p=a+n-1;
for(int i=0;i cout<
16,函数指针
#include
void main(){
int (*fp)(const char *,...)=printf;
fp("hello\n");
}
17,
#include
#include
#include
using namespace std;
int comp(const void*, const void*);
void main(int argc, char *argv[]){
qsort(argv+1, argc-1, sizeof argv[0], comp);
while(--argc)
cout<<*++argv<}
int comp(const void *p1, const void *p2){
const char *ps1=*(const char **)p1;
const char *ps2=*(char **)p2;
return strcmp(ps1,ps2);
}
18,
#include
extern void retrieve(void);
extern void insert(void);
extern void update(void);
extern int show_menu(void);
void main(){
int choice;
void (*farray[])(void)={retrieve,insert,update};
for(;;){
choice=show_menu();
if(choice>=1 && choice<=3)
farray[choice-1]();
else if(choice==4)
break;
}
}
19,
#include
using namespace std;
class Object{
public:
void retrieve(){
cout<<"Object::retrieve"< }
void insert(){
cout<<"Object::insert"< }
void update(){
cout<<"Object::update"< }
void process(int choice);
private:
typedef void (Object::*Omf)();
static Omf farray[3];
};
Object::Omf Object::farray[3]={&Object::retrieve,&Object::insert,&Object::update};
void Object::process(int choice){
if(0<=choice && choice<=2)
(this->*farray[choice])();
}
void main(){
int show_menu(); // 您所提供的!
Object o;
for(;;){
int choice=show_menu();
if(1<=choice && choice<=3)
o.process(choice-1);
else if(choice==4)
break;
}
}
1,标准C允许非void类型指针和void类型指针之间进行直接的相互转换。但在C++中,可以把任何类型的指针直接指派给void类型指针,因为void*是一种通用指针;而不能反过来将void类型指针直接指派给任何非void类型指针,除非强制。
2,不可以直接把基类对象直接转换为派生类对象,无论是直接赋值还是强制转换,因为这是"不自然"的。对于基本类型的强制转换一定要区分值的截断与内存截断的不同。如果你是坚持要使用强制转换,必须同时确保内存访问的安全性和转换结果的安全性。
3,while(getch()!='a'); // 特定终止
4,在&&与||表达式中,如果计算前半部分的值后就能确定表达式的值,则表达式的后半部分不计算。
5,如果两个浮点数之差的绝对值小于或等于某一个可接受的误差(即精度),就认为它们是相等的,否则就是不相等。精度根据具体应用要求而定。因此不要直接用"=="或"!="对两个浮点数进行比较。
EPSILON=1e-6;
if(abs(x-y)<=EPSILON) ... // equal
if(abs(x-y)>EPSILON) ... // not equal
if(abs(x)<=EPSILON) ... // equal 0
if(abs(x)>EPSILON) ... // not equal 0
6, #define NULL ((void *)0)
if(p==NULL) ...
if(p!=NULL) ...
7,在多重嵌套循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,这样可以减少CPU跨切循环层的次数,从而优化程序的性能。
for(col=0;col<5;col++){
for(row=0;row<100;row++){
sum+=a[row][col];
}
}
8,如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体外面。
for(i=0;i<N;i++){
if(condition)
doSomething();
else
doOtherthing();
}
==>>
if(condition){
for(i=0;i<N;i++)
doSomething();
}else{
for(i=0;i<N;i++)
doOtherthing();
}
9,类中的枚举常量
class A{
...
enum{SIZE1=100,SIZE2=200};
int array1[SIZE1];
int array2[SIZE2];
...
};
枚举常量不会占用对象的存储空间,它们在编译时被全部求值,更何况它定义的是一个匿名枚举类型。枚举常量的缺点是不能表示浮点数和字符串。还可以使用另一种方法来定义类的所有对象都共享的常量,即static const。
10,函数调用中参数传递的本质就是用来初始化形参而不是替换形参。
11, char ch='a';
printf("Address of ch:%p\n",&ch);
12,把任何类型的地址或指针强制转换成void *,就可以输出地址了。
13,标准C把空的参数列表解释为可以接受任何类型和个数的参数;而标准C++则把空的参数列表解释为不可以接受任何参数。
void setValue(int width,int height);
float getValue(void);
14,一般地,输出参数放在前面,输入参数放在后面,并且不要交叉出现。
15,应避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型和顺序搞错。此时,可以将这些参数封装为一个对象并采用地址传递或引用传递方式。
16,标准C语言中,凡不加类型说明的函数,一律自动按int类型处理。
17,不要将正常值和错误标志混在一起返回。建议正常值用输出参数获得,而错误标志用return语句返回。