C++ 对象模型具体评论(特别easy理解力)
一、指针与引用
一 概括
指针和引用,在C++的软件开发中非经常见,假设能恰当的使用它们可以极大的提 高整个软件的效率,可是非常多的C++学习者对它们的各种使用情况并非都了解,这就导致了实际的软件开发中经常会内存泄漏,异常抛出,程序崩溃等问题。对 于C和C++的刚開始学习的人。那更是被它们搞的迷迷糊糊。本篇作为[深入C++]系列的第一节,我们就带领大家把指针和引用这个基本功练好。
二 指针
指针,指针的定义是什么呢?好像要想给个直接的定义还是非常难的哦。所以我们这里用它的语法结合图来认识它。
int i = 10;int *p = NULL;p = &i;int j = *p; int **pP = NULL; pP = &p;
在上面的几条语句中,&用来定义引用变量或对变量取其地址。*用来定义指针或得到指针所指向的变量,当中p为定义的指针变量,它指向int变量i。而pP为二级指针变量,它指向指针变量p。
对应的示意图例如以下:
C++是对C的扩展,我们首先来看指针在C中的使用。以下的经典实例来自林锐的 《高质量编程》。记住函数的默认參数传递方式为按值传递,即实參被传入函数内时进行了拷贝。函数内事实上是对拷贝对象的操作。还有当函数使用return返 回变量时,事实上返回的是原对象的一个拷贝。此时的实參和原对象有可能是一般变量也有可能是指针变量。
#pragma once
#include <cstring>
#include <cstdio>
#include <cstdlib>
// -----------------------------------------------
void GetMemory1(char *p, int num)
{
p = (char*)malloc(num);
}
void Test1(void)
{
char *str = NULL;
GetMemory1(str, );
strcpy(str, "hello world");
printf(str);
}
// -----------------------------------------------
void GetMemory2(char **p, int num)
{
*p = (char*)malloc(num);
}
void Test2(void)
{
char * str = NULL;
GetMemory2();
strcpy(str, "hello world");
printf(str);
free(str);
}
// -----------------------------------------------
char* GetMemory3(void)
{
char p[] ="hello world";
return p;
}
void Test3(void)
{
char* str = NULL;
str = GetMemory3();
printf(str);
}
// -----------------------------------------------
char* GetMemory4(void)
{
char *p = "hello world";
return p;
}
void Test4()
{
char* str = NULL;
str = GetMemory4();
printf(str);
}
// -----------------------------------------------
char* GetMemory5(void)
{
);
strcpy(p,"hello world");
return p;
}
void Test5()
{
char* str = NULL;
str = GetMemory5();
printf(str);
free(str);
}
// -----------------------------------------------
void Test6( void )
{
);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world" );
printf(str);
}
}
// -----------------------------------------------
void TestPointerAndReference()
{
// -----------------------------------------------
// 请问执行Test1函数会有什么样的结果?
//
// 答:程序崩溃。同一时候有内存泄漏。
//
// 由于在GetMemory1函数的调用过程中。事实上是对实參指针p做了拷贝,拷贝为局部变量,
// 在函数内的操作是对局部变量的操作,局部变量与实參是两个不同的变量。相互不影响。
//
// 所以,当GetMemory1调用结束时。Test1函数中的 str一直都是 NULL。
// strcpy(str, "hello world");将使程序崩溃。
//
//Test1();
// -----------------------------------------------
// 请问执行Test2函数会有什么样的结果?
//
// 答:(1)可以输出hello world; (2)可是调用结束要对内存释放。否则内存泄漏;
//
Test2();
// -----------------------------------------------
// 请问执行Test3函数会有什么样的结果?
//
// 答:可能是乱码。
//
// 由于GetMemory3返回的是指向“栈内存”的指针,
// 该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。
//
Test3();
// -----------------------------------------------
// 请问执行Test4函数会有什么样的结果?
//
// 答:(1)可以输出hello world; (2) 此时的str指向了常量区。不须要程序猿释放。程序结束自己主动释放。
//
Test4();
// -----------------------------------------------
// 请问执行Test5函数会有什么样的结果?
//
// 答:(1)可以输出hello world; (2)可是调用结束要对内存释放,否则内存泄漏;
//
Test5();
// -----------------------------------------------
// 请问执行Test6函数会有什么样的结果?
//
// 答:篡修改态内存区的内容,后果难以预料,很危急。
//
// 由于free(str);之后,str成为野指针,
// if(str != NULL)语句不起作用。
//
Test6();
}
三 C++指针与引用
引用。事实上是变量的别名,与变量是同一个东东。比如 int i = 10; int &a = i; int &b = i; 这样 a。b为i的引用,即a。b为i的别名,还有 int * pi = new int(10); int *& pa = pi; int *& pb = pi; 此时pa。pb为pi的别名。
在C++中引入了引用概念后。我们不仅能够定义引用变量,对应的函数的传递方式也添加了按引用传递,当參数以引用方式传递 时,函数调用时不正确实參进行拷贝,传入函数内的变量与实參是同一个变量。
以下的实例演示了指针和引用在C++的使用。
#pragma once
#include <iostream>
class Point
{
public:
Point(int x, int y)
{
_x = x;
_y = y;
}
void SetX(int x)
{
_x = x;
}
void SetY(int y)
{
_y = y;
}
void PrintPoint()
{
std::cout << "The Point is : "<< '(' << _x << ',' << _y << ')' << std::endl;
}
void PrintPointAdress()
{
std::cout << "The Point's adress is : " << this << std::endl;
}
private:
int _x;
int _y;
};
// 默认按值传递,当传入时对对像进行了拷贝,函数内仅仅是对所拷贝值的改动,所以实參没被改动。
void ChangeValue(Point pt, int x, int y)
{
pt.SetX(x);
pt.SetY(y);
}
// 按引用传递。函数内外同一值,所以改动了实參。
void ChangeValueByReference(Point& pt, int x, int y)
{
pt.SetX(x);
pt.SetY(y);
}
// 通过传递指针。尽管实參指针传入时也产生了拷贝,可是在函数内通过指针任然改动了指针所指的值。
void ChangeValueByPointer(Point *pt, int x, int y)
{
pt->SetX(x);
pt->SetY(y);
}
void TestChangeValue()
{
Point pt();
pt.PrintPoint();
ChangeValue(pt,);
pt.PrintPoint();
ChangeValueByReference(pt,);
pt.PrintPoint();
ChangeValueByPointer();
pt.PrintPoint();
}
// 按引用传递,所以指针能够被返回。
void ChangePointerByReference(Point *& pPt, int x, int y)
{
pPt = new Point(x, y);
}
// 对二级指针拷贝,可是二级指针指向的一级指针被返回。
void ChangePointerByTwoLevelPointer(Point **pPt, int x, int y)
{
*pPt = new Point(x, y);
}
void TestChangePointer()
{
Point *pPt = NULL;
ChangePointerByReference(pPt, );
pPt->PrintPoint();
pPt->PrintPointAdress();
delete pPt;
pPt = NULL;
);
//int *p2 = new int(10);
//int *p3 = new int(10);
ChangePointerByTwoLevelPointer();
pPt->PrintPoint();
pPt->PrintPointAdress();
delete pPt;
pPt = NULL;
}
void TestPointerAndReference2()
{
TestChangeValue();
TestChangePointer();
}
执行结果例如以下:
四 函数參数传递方式。函数中return语句和拷贝构造函数的关系
通过上面的2个实例,假设还有人对函数的參数传递方式和return有疑问的啊。能够对以下的代码亲自debug。
#pragma once
#include <iostream>
class CopyAndAssign
{
public:
CopyAndAssign(int i)
{
x = i;
}
CopyAndAssign(const CopyAndAssign& ca)
{
std::cout << "拷贝构造!
" << std::endl;
x = ca.x;
}
CopyAndAssign& operator=(const CopyAndAssign& ca)
{
std::cout << "赋值操作符" << std::endl;
x = ca.x;
return *this;
}
private:
int x;
};
CopyAndAssign ReturnCopyAndAssign()
{
CopyAndAssign temp(); // 构造
return temp;
}
void CopyAndAssignAsParameter(CopyAndAssign ca)
{
}
CopyAndAssign& ReturnCopyAndAssignByReference()
{
CopyAndAssign temp(); // 构造
return temp;
}
void CopyAndAssignAsParameterByReference(CopyAndAssign& ca)
{
}
void TestCopyAndAssign()
{
CopyAndAssign c1(); // 构造
CopyAndAssignAsParameter(c1); // 拷贝构造
ReturnCopyAndAssign(); // 拷贝构造
CopyAndAssignAsParameterByReference(c1);
ReturnCopyAndAssignByReference();
}
亲自debug,效果会更好。执行结果例如以下:
五 总结
1) 指针也是变量,它存储其它变量的地址。比如int *p = new int(10); p是指针变量,p实际是存储了一个int变量的地址。
2)引用事实上是一个别名。跟原对象是同一个东东。比如 std::string str = "hello"; std::string & strR = str;此时strR跟str事实上是同一个东东,strR能够看成是str的一个小名。
3) 函数默认的传參方式为按值传递,即当实參传入是事实上是做了拷贝,函数内事实上是对所拷贝对象的操作。比如 void Increase(int x) { x++; } 调用时 int i = 10; Increase(i); Increase函数内部事实上是对i的一个拷贝(我们如果为ii)进行++。
所以在函数调用结束后原来的i的值仍然保持不变。
4)函数的传參方式 能够显示的指定按引用来传递,按引用传递时,函数内即对实參的操作。没有拷贝操作,所以函数内对实參的改动。当然后调用结束后反映到实參上。比如void Increase(int & x) { x++;} 调用 int i = 10; Increase(i);此时Increase内部的++即是对i的操作,所以函数调用结束后i的值被改动。
5)函数中假设有return返回变量时,事实上所返回的也是一个拷贝。所以当使用return返回对象时一定要考虑所返回对象的拷贝构造函数是否可以满足要求。
六 使用注意
1) malloc/free一起使用。
2)new/delete一起使用。
3)对于new中有[]时,对应的必须使用delete[]来释放。
4)用free或delete释放了内存之后。马上将指针设置为NULL,防止产生“野指针”。
5)对指针的使用前,应该检查是否为空,空指针可能导致程序崩溃。
6)非内置类型的參数传递,使用const引用取代一般变量。
七 谢谢!
二 指针与数组
一 C指针操作函数
new和delete对C++的程序猿或许非常熟悉,可是malloc和free被用来在C代码中用来内存分配和释放,非常多C++开发人员并不能游刃有余的使用,以下实例解析malloc和free的使用。
malloc | void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针。假设分配失败,则返回一个空指针(NULL)。 |
free | void free(void *FirstByte): 该函数是将之前用malloc分配的空间还给程序或者是操作系统。也就是释放了这块内存,让它又一次得到自由。 |
实比例如以下:
#pragma once
#include <string>
void TestMallocAndFree()
{
char *ptr = NULL;
ptr * sizeof(char));
if (NULL == ptr)
{
return;
}
memcpy(ptr,"Hello!",strlen("Hello!"));
free(ptr);
ptr = NULL;
typedef struct data_type
{
int age;
char *name;
} data;
data *willy = NULL;
willy = (data*) malloc( sizeof(data) );
willy;
willy->name = "jason"; // 此时的name指向了常量区,所以name指针不须要程序猿释放。
free( willy );
willy = NULL;
}
malloc/free 和new/delete的差别:
1)new/delete是保留字,不须要头文件支持. malloc/free须要头文件库函数支持. 使用malloc/free须要包括 #include<cstdlib> 或<stdlib>.
2) new 建立的是一个对象,new会依据对象计算大小。直接返回对象的指针,当使用完成后调用delete来释放,但malloc分配的是一块内存。须要用户制定 所要分配内存的大小,并且返回的均为void的指针,使用时须要对应的强制类型转化。使用结束后调用free来释放内存.
3)new/delete的使用除了分配内存和释放,还调用了类型的构造函数和析构函数。而malloc/free仅仅是简单的分配和释放内存。
二 数组与指针
C++的数组常常须要和指针来结合使用,以下来进行相关的强化训练。实比例如以下:
#pragma once
#include <iostream>
using namespace std;
void PrintArray(double *p, int num)
{
; i < num; ++i)
{
cout << " " << p[i] << " ";
}
cout << endl << "The array is end!" << endl;
}
])
{
; ++i)
{
cout << " " << *(arr+i)/*arr[i]*/ << " ";
}
cout << endl << "The array is end!" << endl;
}
]) // 数组传參为传指针,所以函数内能够改动
{
; ++i)
{
arr[i] ;
}
}
])
{
; ++i)
; ++j)
cout << " " << arr[i][j] << " ";
cout << endl << "The array is end!" << endl;
}
;}
void TestArray()
{
// 数组的定义和初始化
};
];
arr[] = 1.0;
arr[] = 2.0;
arr[] = 3.0;
double arr2[] = {1.0,2.0,3.0};
//double arr3[3] = arr; // error
);
PrintArray();
PrintArray(arr2);
] = {1.0,0.0,0.0,1.0};
] = {{1.0,0.0,0.0},
{0.0,1.0,0.0},
{0.0,0.0,1.0}};
PrintArray(matrix3[);
PrintArray();
//PrintArray(matrix3,3*3);
PrintArray(matrix3);
// 指针来模拟数组
double *p3 = new double[GetLength()];
p3[] = 10.0;
p3[] = 20.0;
p3[] = 30.0;
PrintArray(p3,);
PrintArray(p3);
delete []p3;
// 数组+指针实现二维变长数组
];
p4[];
p4[];
p4[;
p4[;
p4[;
p4[;
p4[;
p4[;
PrintArray(p4[);
PrintArray(p4[);
delete [] p4[];
delete [] p4[];
PrintArray(arr); // 数组传參为传指针,所以函数内能够改动
ChangeArray(arr);
PrintArray(arr);
}
代码分析总结:
1)数组的定义必须使用常量指定长度,比如:double arr[3],可是使用指针时能够是执行时指定。比如double *p3 = new double[getLength()]。
2)数组定义时即分配空间且在栈上,不须要程序猿来对内存管理,可是假设对指针使用了new[]。则必须由程序猿在使用完成后delete[]。
3) 一维数组数组名即第一个元素的地址,比如:double arr[3]中。arr == &arr[0]为true。
4)二维数组中第一行的地址即为第一个元素的地址,比如:double matrix3 [3][3],matrix[0] == &matrix[0][0]为true。
5)能够使用指针数组来模拟变长二维数组,比如:double *p4[2]; p4[0] = new double[2]; p4[1] = new double[4];
6)二维数组内存中同一维数组仍为连续的区域。所以能够将二维数组和一维相互转化。
7)一维数组名即为第一个元素的地址,所以能够同指针隐式转化,但二维数组名不是第一个元素地址,所以不能转化。
8) 当函数传入数组,实际传首元素的指针。所以能够在函数内改动数组元素。
三 完!
感谢。Thanks!
三 指针与字符串
開始之前必须明白strlen的含义。原型为size_t strlen( char *str ); strlen返回字符串的长度,即null("0)之前的字符的数量。
一 char* 与 char []
实例加凝视:
void TestCharPointerAndArray()
{
char *c1 = "abc"; //abc"0常量区,c1在栈上,
常量区程序结束后自己主动释放。
//c1[1] = 'g'; // 常量不能改动
int i = strlen(c1); // 3
char c2[] = "abc"; // c2,abc"0都在栈上
] = 'g'; // 能够改动
int j = strlen(c2); // 3
int jj = sizeof(c2); // 4
* sizeof(char)); // c3
栈上
); // abc"0
在堆上, 4 = 3(strlen("abc")) + 1('"0');
] = 'g'; // 能够改动
int x = strlen(c3); // 3
free(c3); //假设这里不free,会内存泄漏
c3 = "abc"; // abc"0
在常量区,c3指向了常量区
//c3[1] = 'g'; // 常量不能改动
int y = strlen(c3); // 3
}
字符串都以"0结尾,所以比如:char *c1 = "abc";char c2[] = "abc";,使用strlen得到长度都为3。可是实际的存储空间为strlen+1即3+1。
二 C中字符串操作函数
C++的程序猿对C中的字符串指针操作的函数却并非相当的熟悉。
而C中的这些字符串的指针操作函数有的时候也是必需要面对的,比方我们的库要提供 C函数接口。保持向后兼容和跨平台,还有我们常常使用一些第三方的库中都或多或少的使用到了这些C中的指针操作函数,所以以下列出C的指针操作函数,帮助 大家熟悉之。
1) memcpy/memset/memcmp
memcpy |
原型:extern void *memcpy( void *to, const void *from, size_t count ); |
memset |
原型:extern void* memset( void* buffer, int ch, size_t count ); 说明:返回指向buffer的指针。 |
memcmp |
原型:extern int memcmp(const void *buffer1, const void *buffer2, size_t count ); |
memchr |
原型: extern void *memchr( const void *buffer, int ch, size_t count ); 包括:#include <string.h> 或<string>或<cstring> 功能:查找ch在buffer中第一次出现的位置。 说明:假设发现返回指针。假设没有返回NULL。 |
实例:
void TestMemFunction()
{
char *s1="Hello!"; // Hello!"0
int l = strlen(s1); // 6
]; // d1
须要strlen(s1) + 1 空间
);
memcpy(d1,d1,l);
memmove(d1 );
;
char the_array[ARRAY_LENGTH];
// zero out the contents of the_array
memset( the_array, 'c', ARRAY_LENGTH );
char *a1 = "source1";
] = "source2";
); // 仅比較source,所以相等
];
char *ptr;
strcpy(str, "This is a string");
ptr = (char*)memchr(str, 'r',
strlen(str));
}
2) strlen/strcpy/strcat/strcmp/strchr/strcoll/strstr/strtok/strtod/strtol
strcpy |
char *strcpy(char *s1, const char *s2) 将字符串s2拷贝到字符串数组s1中,返回s1的值 |
strcat |
char *strcat(char *s1, const char *s2) s2的第一个字符重定义s1的null终止符。返回s1的值 |
strcmp |
int strcmp(const char *s1, const char *s2) |
strchr |
char *strchr(char * str,int c ); 在str中查找c第一次出现的位置。 |
strstr | char *strstr(char *str,const char *strSearch );在string1中查找string2第一次出现的位置。 |
strtok | char *strtok(char *strToken,const char *strDelimit ); 切割字符串。 |
实例:
void TestStrFunction()
{
];
"; // 123456789"0
strcpy(string, str1);
strcat(string,"A"); //123456789A"0
int r = strcmp(string,"123456789B"); // 123456789A"0 < 123456789B"0
}
void TestStrFunction2()
{
int ch = 'r';
char string[] = "The quick # brown dog # jumps over # the lazy fox";
char *pdest = NULL;
pdest = strchr( string, ch );
pdest = NULL;
char * str = "dog";
pdest = strstr(string,str);
pdest = NULL;
char delims[] = "#";
pdest = strtok( string, delims );
while( pdest != NULL )
{
pdest = strtok( NULL, delims );
}
}
总结:
1)以mem開始的函数用来bytes的操作,所以须要指定长度,可是以str用来操作以"0结尾的字符串,不须要指定长度。
2)对于unicode。对应的字符串操作函数前缀为wcs。比如wcscpy,wcscat,wcscmp,wcschr,wcsstr。wcstok等。
3)在vc中还提供了有安全检測的字符串函数后缀_s,比如strcpy_s,strcat_s,strcmp_s,wcscpy_s,wcscat_s,wcscmp_s等。
4)char*假设指向常量区。不能被改动,且此char*不须要delete。比如 char* pStr = "ABC";。
三 std::string和std::wstring使用相当简单哦!
四 完!
感谢,Thanks!
四 堆栈与函数调用
一 C++程序内存分配
1) 在栈上创建。在运行函数时,函数内局部变量的存储单元都在栈上创建,函数运行结束时这些存储单元自己主动被释放。栈内存分配运算内置于处理器的指令集中,一般使用寄存器来存取。效率非常高,可是分配的内存容量有限。
2) 从堆上分配。亦称动态内存分配。程序在执行的时候用malloc或new申请随意多少的内存,程序猿自己负责在何时用free或delete来释放内存。动态内存的生存期由程序猿自己决定,使用很灵活。
3) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个执行期间都存在。
比如全局变量。static变量。
4) 文字常量分配在文字常量区,程序结束后由系统释放。
5)程序代码区。
经典实例:(代码来自网络高手,没有找到原作者)
#include <string>
; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b;//栈
char s[]="abc"; //栈
char *p2; //栈
"; //123456"0在常量区,p3在栈上。
; //全局(静态)初始化区
);
p2 ); //分配得来得10和20字节的区域就在堆区。
"); //123456"0放在常量区,编译器可能会将它与p3所向"123456"0"优化成一个地方。
}
二 三种内存对象的比較
栈对象的优势是在适当的时候自己主动生成。又在适当的时候自己主动销毁。不须要程序猿担心;并且栈对象的创建速度一般 较堆对象快,由于分配堆对象时。会调用operator new操作。operator new会採用某种内存空间搜索算法。而该搜索过程可能是非常费时间的,产生栈对象则没有这么麻烦,它只须要移动栈顶指针就能够了。可是要注意的是,通常栈 空间容量比較小,通常是1MB~2MB。所以体积比較大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象。由于随着递归调用深度的添加。 所需的栈空间也会线性添加。当所需栈空间不够时。便会导致栈溢出,这样就会产生执行时错误。
堆对象创建和销毁都要由程序猿负责。所以,假设 处理不好。就会发生内存问题。假设分配了堆对象,却忘记了释放,就会产生内存泄漏;而如 果已释放了对象,却没有将对应的指针置为NULL。该指针就是所谓的“悬挂指针”,再度使用此指针时。就会出现非法訪问,严重时就导致程序崩溃。
可是高效 的使用堆对象也能够大大的提高代码质量。比方。我们须要创建一个大对象。且须要被多个函数所訪问,那么这个时候创建一个堆对象无疑是良好的选择,由于我们 通过在各个函数之间传递这个堆对象的指针。便能够实现对该对象的共享,相比整个对象的传递,大大的减少了对象的拷贝时间。另外,相比于栈空间,堆的容量要
大得多。
实际上,当物理内存不够时,假设这时还须要生成新的堆对象。通常不会产生执行时错误。而是系统会使用虚拟内存来扩展实际的物理内存。
静态存储区。全部的静态对象、全局对象都于静态存储区分配。
关于全局对象,是在main()函数运行前就分配好了的。事实上。在main()函数中的显示代 码运行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行全部全局对象的的构造及初始化工作。而在main()函数结束之 前,会调用由编译器生成的exit函数。来释放全部的全局对象。比方以下的代码:
void main(void) |
实际上,被转化成这样:
void main(void) |
除了全局静态对象,还有局部静态对象通和class的静态成员。局部静态对象是在函数中定义的。就像栈对象一样,仅仅只是,其前面多了个 statickeyword。局部静态对象的生命期是从其所在函数第一次被调用。更确切地说。是当第一次运行到该静态对象的声明代码时,产生该静态局部对象。直到 整个程序结束时,才销毁该对象。class的静态成员的生命周期是该class的第一次调用到程序的结束。
三 函数调用与堆栈
1)编译器一般使用栈来存放函数的參数,局部变量等来实现函数调用。有时候函数有嵌套调用,这个时候栈中会有多个函数的信息,每一个函数占用一个连续的区域。
一个函数占用的区域被称作帧(frame)。同一时候栈是线程独立的,每一个线程都有自己的栈。
比如以下简单的函数调用:
另外函数堆栈的清理方式决定了当函数调用结束时由调用函数或被调用函数来清理函数帧。在VC中对函数栈的清理方式由两种:
參数传递顺序 | 谁负责清理參数占用的堆栈 | |
__stdcall | 从右到左 | 被调函数 |
__cdecl | 从右到左 | 调用者 |
2) 有了上面的知识为铺垫。我们以下细看一个函数的调用时堆栈的变化:
代码例如以下:
int Add(int x, int y)
{
return x + y;
}
void main()
{
);
);
;
result = Add(*pi,*pj);
delete pi;
delete pj;
}
对上面的代码,我们分为四步,当然我们仅仅画出了我们的代码对堆栈的影响。其它的我们如果它们不存在。哈哈!
第一,;
堆栈变化例如以下:
第二,Add(*pi,*pj);堆栈例如以下:
第三,将Add的结果给result,堆栈例如以下:
第四。delete pi; delete pj; 堆栈例如以下:
第五。当main()退出后。堆栈例如以下,等同于main运行前,哈哈!
四 完!
感谢,Thanks!
五 sizeof与内存布局
有了前面几节的铺垫,本节開始摸索C++的对象的内存布局,平台为windows32位+VS2008。
一 内置类型的size
内置类型,直接上代码。帮助大家加深记忆:
void TestBasicSizeOf()
{
cout << __FUNCTION__ << endl;
cout << " sizeof(char)= " << sizeof ( char ) << endl;
cout << " sizeof(int)= " << sizeof ( int ) << endl;
cout << " sizeof(float)= " << sizeof ( float ) << endl;
cout << " sizeof(double)= " << sizeof ( double ) << endl;
cout << " sizeof('$')=" << sizeof ( '$' ) << endl;
cout ) << endl;
cout << " sizeof(1.5f)= " << sizeof ( 1.5f ) << endl;
cout << " sizeof(1.5)= " << sizeof ( 1.5 ) << endl;
cout << " sizeof(Good!)= " << sizeof ( "Good!" ) << endl
;
char str[] = "CharArray!";
];
];
cout << " char str[] = ""CharArray!""," << " sizeof(str)= " << sizeof (str) << endl;
cout << " int a[10]," << " sizeof(a)= " << sizeof (a) << endl;
cout << " double xy[10]," << " sizeof(xy)= " << sizeof (xy) << endl;
cout << " sizeof(void*)= " << sizeof(void*) << endl;
}
执行结果例如以下:
二 struct/class的大小
在C++中我们知道struct和class的唯一差别就是默认的訪问级别不同,struct默觉得public,而class的默觉得 private。所以考虑对象的大小。我们均以struct为例。
对于struct的大小对于刚開始学习的人来说还确实是个难回答的问题,我们就通过以下的一个 struct定义加逐步的变化来引出相关的知识。
代码例如以下:
struct st1
{
short number;
float math_grade;
float Chinese_grade;
float sum_grade;
char level;
}; //20
struct st2
{
char level;
short number;
float math_grade;
float Chinese_grade;
float sum_grade;
};//16
#pragma pack(1)
struct st3
{
char level;
short number;
float math_grade;
float Chinese_grade;
float sum_grade;
}; //15
#pragma pack()
void TestStructSizeOf()
{
cout << __FUNCTION__ << endl;
cout << " sizeof(st1)= " << sizeof (st1) << endl;
cout << " offsetof(st1,number) " << offsetof(st1,number) << endl;
cout << " offsetof(st1,math_grade) " << offsetof(st1,math_grade) << endl;
cout << " offsetof(st1,Chinese_grade) " << offsetof(st1,Chinese_grade) << endl;
cout << " offsetof(st1,sum_grade) " << offsetof(st1,sum_grade) << endl;
cout << " offsetof(st1,level) " << offsetof(st1,level) << endl;
cout << " sizeof(st2)= " << sizeof (st2) << endl;
cout << " offsetof(st2,level) " << offsetof(st2,level) << endl;
cout << " offsetof(st2,number) " << offsetof(st2,number) << endl;
cout << " offsetof(st2,math_grade) " << offsetof(st2,math_grade) << endl;
cout << " offsetof(st2,Chinese_grade) " << offsetof(st2,Chinese_grade) << endl;
cout << " offsetof(st2,sum_grade) " << offsetof(st2,sum_grade) << endl;
cout << " sizeof(st3)= " << sizeof (st3) << endl;
cout << " offsetof(st3,level) " << offsetof(st3,level) << endl;
cout << " offsetof(st3,number) " << offsetof(st3,number) << endl;
cout << " offsetof(st3,math_grade) " << offsetof(st3,math_grade) << endl;
cout << " offsetof(st3,Chinese_grade) " << offsetof(st3,Chinese_grade) << endl;
cout << " offsetof(st3,sum_grade) " << offsetof(st3,sum_grade) << endl;
}
执行结果例如以下;
基于上面的对struct的測试。我们是不是有些惊呆哦。对于C++的刚開始学习的人更是不由自主的说:“我靠。原来顺序不同所占空间都不同啊。还有那个 pack是啥东东啊?”,事实上这里蕴含了一个内存对齐的问题。在计算机的底层进行内存的读写的时候。假设内存对齐的话能够提高读写效率,以下是VC的默认 规则:
1) 结构体变量的首地址可以被其最宽基本类型成员的大小所整除;
2) 结构体每一个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍。 如有须要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍。如有须要编译器会在最末一个成员之后加上填充字节(trailing padding)。
当然VC提供了project选项/Zp[1|2|4|8|16]能够改动对齐方式,当然我们也能够在代码中对部分类型实行特殊的内存对齐方式。改动方式为#pragma pack( n ),n为字节对齐
数,其取值为1、2、4、8、16,默认是8。取消改动用#pragma pack(),假设结构体某成员的sizeof大于你设置的,则按你的设置来对齐。
三 struct的嵌套
1)实例:
struct A
{
int i;
char c;
double d;
short s;
}; // 24
struct B
{
char cc;
A a;
int ii;
}; // 40
布局:(使用VS的未公布的编译选项/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)
2)实例:
#pragma pack(4)
struct A2
{
int i;
char c;
double d;
short s;
}; // 20
#pragma pack()
struct B2
{
char cc;
A2 a;
int ii;
}; // 28
布局:(使用VS的未公布的编译选项/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)
总结:
因为结构体的成员能够是复合类型,比方另外一个结构体,所以在寻找最宽基本类型成员时,应当包含复合类型成员的子成员。而不是把复合成员看成是一个总体。但在确定复合类型成员的偏移位置时则是将复合类型作为总体看待。
四 空struct/class和const。static成员
实例:
struct empty{}; // 1
struct constAndStatic
{
const int i;
static char c;
const double d;
static void TestStatic(){}
void TestNoStatic(){}
}; // 16
布局:(使用VS的未公布的编译选项/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)
上面的实例中empty的大小为1。而constAndStatic的大小为16。
总结:
由于static成员和函数事实上是类层次的。不在对象中分配空间,而成员函数事实上是被编译为全局函数了,所以也不在对象中。
五 本节完,下次探讨虚函数对内存布局的影响!
感谢,Thanks!
六 单继承与虚函数表
一 单继承
1) 代码:
#include <iostream>
using namespace std;
class A
{
public:
void f1(){cout << "A::f1" << endl;}
void f2(){cout << "A::f2" << endl;}
virtual void v1(){cout << "A::v1" << endl;}
virtual void v2(){cout << "A::v2" << endl;}
int x;
};
class B : public A
{
public:
void f2(){cout << "B::f2" << endl;} // 覆盖
void v2(){cout << "B::v2" << endl;} // 重写
void f3(){cout << "B::f3" << endl;}
virtual void v3(){cout << "B::v3" << endl;}
int y;
};
class C : public B
{
public:
void f3(){cout << "C::f3" << endl;} // 覆盖
void v1(){cout << "C::v1" << endl;} // 重写
void v3(){cout << "C::v3" << endl;} // 重写
int z;
};
2)类图:
3)VS2008的编译选项查看布局:
4)可视化表示:
5)代码验证:
typedef void (*Fun)();
void PrintVTable(A *pA)
{
int *pVT = (int*)*(int*)(pA);
Fun);
;
while(*pF != NULL)
{
(*pF)();
++iLength;
pF = (Fun*)(pVT + iLength);
}
}
void PrintMembers(A *pA)
{
int *p = (int*)(pA);
;
)
{
cout << *(p+i) << endl;
i++;
}
}
void TestVT()
{
A *pA = new C();
C *pC = dynamic_cast<C*>(pA);
pC;
pC;
pC;
PrintVTable(pA);
PrintMembers(pA);
delete pA;
}
6)验证代码执行结果:
7)总结:
单继承的对象的布局,第一个为虚函数表指针vtbl,其后为成员且先基类后子类,虚函数表里包括了全部的虚函数的地址,以NULL结束。虚函数假设子类有重写,就由子类的又一次的取代。
二 单继承执行时类型转化
1)代码验证:
void TestDynamicCast()
{
A *pA = new C();
cout << "A:" << pA << endl;
B *pB = dynamic_cast<B*>(pA);
cout << "B:" << pB << endl;
C *pC = dynamic_cast<C*>(pA);
cout << "C:" << pC << endl;
}
2)验证代码执行结果:
3)总结:
我们上面看了单继承的内存布局,而这种内存布局也就决定了当dynamic_cast的时候。都还是同一地址,不须要做指针的移动。仅仅是类型的改变即所能訪问的范围的改变。
三 完!
感谢,Thanks!
七 多继承与虚函数表
一 多重继承
1) 代码:
#include <iostream>
using namespace std;
class B1
{
public:
int x;
virtual void v1(){ cout << "B1::v1" << endl; }
void f1(){cout << "B1::f1" << endl; }
};
class B2
{
public:
int y;
virtual void v2(){ cout << "B2::v2" << endl; }
void f2(){ cout << "B2::f2" << endl; }
};
class B3
{
public:
int z;
virtual void v3(){ cout << "B3::v3" << endl; }
void f3(){ cout << "B3::f3" << endl; }
};
class D : public B1, public B2, public B3
{
public:
int a;
void v3(){ cout << "D::v3" << endl; }
virtual void vD(){ cout << "D::vD" << endl; }
};
2)类图:
3)VS2008的编译选项查看布局:
4)可视化表示:
5)代码验证:
typedef void (*Fun)();
void PrintMember(int *pI)
{
cout << *pI << endl;
}
void PrintVT(int *pVT)
{
while(*pVT != NULL)
{
(*(Fun*)(pVT))();
pVT++;
}
}
void PrintVTAndMember(B1 *pD)
{
int *pRoot = (int*)pD;
);PrintVT(pVTB1);
; PrintMember(pMB1);
);PrintVT(pVTB2);
; PrintMember(pMB2);
);PrintVT(pVTB3);
; PrintMember(pMB3);
}
void TestVT()
{
B1 *pB1 = new D();
D *pD = dynamic_cast<D*>(pB1);
pD;
pD;
pD;
pD;
PrintVTAndMember(pD);
delete pD;
}
6) 验证代码执行结果:
7)总结:
与单继承同样的是全部的虚函数都包括在虚函数表中。所不同的多重继承有多个虚函数表。当子类对父类的虚函数有重写时,子类的函数覆盖父类的函数在相应的虚函数位置,当子类有新的虚函数时,这些虚函数被加在第一个虚函数表的后面。
二 多重继承执行时类型转化
1)代码验证:
void TestDynamicCast()
{
B1 *pB1 = new D();
cout << "B1:" << pB1 << endl;
D *pD = dynamic_cast<D*>(pB1);
cout << "D:"<< pD << endl;
B2 *pB2 = dynamic_cast<B2*>(pB1);
cout << "B2:" << pB2 << endl;
B3 *pB3 = dynamic_cast<B3*>(pB1);
cout << "B3:" << pB3 << endl;
delete pD;
}
2)验证代码的执行结果:
3)总结:
从多重继承的内存布局,我们能够看到子类新增加的虚函数被加到了第一个基类的虚函数表,所以当dynamic_cast的时候,子类和第一个基类的地址同样,不须要移动指针,可是当dynamic_cast到其它的父类的时候,须要做对应的指针的移动。
三 完。
感谢。Thanks!
八 虚继承与虚函数表
一 虚继承
1) 代码:
#include <iostream>
using namespace std;
class B
{
public:
int i;
virtual void vB(){ cout << "B::vB" << endl; }
void fB(){ cout << "B::fB" << endl;}
};
class D1 : virtual public B
{
public:
int x;
virtual void vD1(){ cout << "D1::vD1" << endl; }
void fD1(){ cout << "D1::fD1" << endl;}
};
class D2 : virtual public B
{
public:
int y;
void vB(){ cout << "D2::vB" << endl;}
virtual void vD2(){ cout << "D2::vD2" << endl;}
void fD2(){ cout << "D2::fD2" << endl;}
};
class GD : public D1, public D2
{
public:
int a;
void vB(){ cout << "GD::vB" << endl;}
void vD1(){cout << "GD::vD1" << endl;}
virtual void vGD(){cout << "GD::vGD" << endl;}
void fGD(){cout << "GD::fGD" << endl;}
};
2)类图:
3)VS2008的编译选项查看布局:
4)可视化表示:
5)代码验证:(此时的虚函数表不是以NULL结尾,为什么?)
typedef void (*Fun)();
void PrintMember(int *pI)
{
cout << *pI << endl << endl;
}
void PrintVT(int *pVT)
{
while(*pVT != NULL)
{
(*(Fun*)(pVT))();
pVT++;
}
}
void PrintMemberAndVT(GD *pGD)
{
int *pRoot = (int*)pGD;
);
())();
); cout << "vbtable's adress:" << *pVB << endl;
); PrintMember(pX);
);
(*(Fun*)(pD2VT))();
); cout << "vbtable's adress:" << *pVB2 << endl;
); PrintMember(pY);
); PrintMember(pA);
);
(*(Fun*)(pBVT))();
); PrintMember(pI);
}
void TestVT()
{
B *pB = new GD();
GD *pGD = dynamic_cast<GD*>(pB);
pGD;
pGD;
pGD;
pGD;
PrintMemberAndVT(pGD);
delete pGD;
}
6)验证代码结果:
7)总结:
虚继承。使公共的基类在子类中仅仅有一份,我们看到虚继承在多重继承的基础上多了vbtable来存储到公共基类的偏移。
二 虚继承执行时类型转化
1)代码验证:
void TestDynamicCast()
{
B *pB = new GD();
GD *pGD = dynamic_cast<GD*>(pB);
cout << "GD:" << pGD << endl;
D1 *pD1 = dynamic_cast<D1*>(pB);
cout << "D1:" << pD1 << endl;
D2 *pD2 = dynamic_cast<D2*>(pB);
cout << "D2:" << pD2 << endl;
cout << "B:" << pB << endl;
}
2)验证代码结果:
3)总结:
还是从内存布局来看dynamic_cast时地址的变化,第一个基类的地址与子类同样。其它的基类和虚基类须要做偏移。
三 完!
感谢,Thanks。
九 类型转换
一 typeid与dynamic_cast
1)RTTI, Runtime Type Identification (RTTI) or Run-time type information (RTTI),表示在执行时动态决定变量的类型。来调用正确的虚函数。 RTTI在VS2008中默觉得关闭,能够通过改动编译选项Enable Run-Time Type Info 为 Yes,来启用RTTI。仅仅有当启动RTTI时。用来RTTI功能的typeid和dynamic_cast才干正常工作。
2)type_info。用来描写叙述类型信息。type_info存储了它所描写叙述的类型的名字。RTTI就是使用type_info来实现的。type_info的定义例如以下:
class type_info {
public:
virtual ~type_info();
bool operator== (const type_info& rhs) const;
bool operator!= (const type_info& rhs) const;
bool before (const type_info& rhs) const;
const char* name() const;
private:
type_info (const type_info& rhs);
type_info& operator= (const type_info& rhs);
};
问题:RTTI怎么实现那?对象。type_info,虚函数怎么关联那?《深入C++对象模型》中说在虚函数表的開始存储了类型信息,可是实际的VS2008中好像并没有此信息。请高人指点哦!
3)typeid,在执行时获得对象的类型,typeid()返回的是const type_info&。而 type_info包括了对象真实类型的名字。typeid能被用来获取一个引用对象或指针指向的对象的执行时的真实类型。当然假设对象为null或编译 时没有使用/GR的话。typeid的会抛出异常bad_typeid exception或__non_rtti_object。
实例代码:
class Base
{
public:
virtual void f(){ }
};
class Derived : public Base
{
public:
void f2() {}
};
void main ()
{
Base *pB = new Derived();
const type_info& t = typeid(*pB);cout <<t.name() << endl;
delete pB;
Derived d;
Base& b = d;
cout << typeid(b).name() << endl;
}
执行结果:
4)dynamic_cast,用来执行时的类型转化,须要/GR来正确执行。
适用:
第一,用于全部的父子和兄弟间指针和引用的转化,有类型安全检查;
第二,对指针类型。假设不成功,返回NULL。对引用类型,假设不成功。则抛出异常;
第三,类型必需要有虚函数。且打开/GR编译选项。否则不能使用dynamic_cast。
实例代码:
class AA
{
public:
virtual void do_sth(){ std::cout<<"AA"n"; }
};
class BB
{
public:
virtual void do_sth(){ std::cout<<"BB"n"; }
};
class CC : public AA, public BB
{
public:
virtual void do_sth(){ std::cout<<"CC"n"; }
};
void DynamicCastTest()
{
AA *pA = new CC;
BB *pB = dynamic_cast<BB*>(pA);
if(pB != NULL)
cout << "cast successful!" << endl;
CC *pC = dynamic_cast<CC*>(pA);
if(pC != NULL)
cout << "cast successful!" << endl;
}
二 其它cast
1)隐式转化。不须要不论什么操作符,转化被自己主动运行。当一个值被赋值到它所兼容的类型时。
适用:
第一,内置基本类型的兼容转化。
第二。 子类指针。引用向父类的转化。
实例:
class A
{
public:
virtual ~A(){}
};
class B : public A
{
};
void ImplicitCast()
{
;
int b;
b = a;
double d = 10.05;
int i;
i = d;
;
char c;
c = j;
A* pA = new B();
}
2)强制类型转化,即我们常说的C风格的类型转化。基本上能够用于全部的转化,可是没有意义的转化除外。可是父子类。兄弟间的转化没有类型检查可能导致执行是错误。
适用:
第一,基本类型转化。
第二,void*到其它指针的转化;
第三,去除const。
第五,函数指针的转化。
第六,父子类转化。可是多重继承和兄弟转化。可能有执行时错误,没有类型检查;
第七,不论什么两个类,可是没有实际意义,执行可能出错;
第八,不能用于没有意义的转化。严厉禁止,比如,你不能用static_cast象用C风格的类型转换一样把struct转换成int类型,或者把double类型转换成指针类型;
第九。在C++一般更推荐新加的static_cast。const_cast,dynamic_cast和reinterpret_cast转化方式。
实例:
class CDummy
{
public:
CDummy(float x, float y)
{
i = x;
j = y;
}
private:
float i,j;
};
class CAddition
{
public:
CAddition (int a, int b) { x=a; y=b; }
int result() { return x+y;}
private:
int x,y;
};
int Testing()
{
std::cout << "Testing" << std::endl;
;
}
void ExplicitCast()
{
;
);
void *pV;
pV = pi;
int *pj = (int*)pV; // 或 int *pj = int*(pV);
);
int *pb;
pb = (int*)pa;
;
std::cout << *pa << std::endl;
typedef void (*Fun)();
Fun f = (Fun)Testing;
f();
// 多重继承或将兄弟间的转化可能会出错
// 尽管能够正确的编译。可是执行有问题。所以我们不做没有意义的转化
//CDummy d(10,30);
//CAddition * padd;
//padd = (CAddition*) &d;
//std::cout << padd->result();
// 不做没有意义的转化
//// error
//struct st{int i; double d;};
//st s;
//int x = (int)s; //c2440
//double y = 10.0;
//int *p = (int*)y; // c2440
}
3)static_cast在功能上基本上与C风格的类型转换一样强大。含义也一样。
它也有功能上限制:
第一,不能兄弟间转化,父子间转化没有类型安全检查。有可能会导致执行时错误,父子兄弟的动态转化应该适用dynamic_cast。
第二,不能去除const,适用专用的const_cast;
第三。不能用于两个没有继承关系的类。当然实际上这种转化也是没有意义的;
第四,当然也不支持没有意义的转化,比如。你不能用static_cast象用C风格的类型转换一样把struct转换成int类型。或者把double类型转换成指针类型。
4)const_cast,用来改动类型的const或volatile属性。
适用:
第一。常量指针被转化成很量指针,而且仍然指向原来的对象;
第二,常量引用被转换成很量引用。而且仍然指向原来的对象。
第三。常量对象被转换成很量对象;
实例:
void ConstCastTest()
{
);
int *pb;
pb = const_cast<int*>(pa);
;
std::cout << *pa << std::endl;
}
5)reinterpret_cast,此转型操作符的结果取决于编译器。用于改动操作数类型,非类型安全的转换符。
适用:
一般不推荐使用,可是一般用来对函数指针的转化。
实例:
// 不能够移植,不推荐使用
int ReinterpretTest()
{
struct dat { short a; short b;};
long value = 0x00100020;
dat * pd = reinterpret_cast<dat *> (&value);
std::cout << pd->a << std::endl; // 0x0020
std::cout << pd->b << std::endl; // 0x0010
;
}
typedef void (*Fun)();
int Testing()
{
std::cout << "Testing" << std::endl;
;
}
void ReinterpretTest2()
{
//Fun f = (Fun)Testing;
//f();
Fun f = reinterpret_cast<Fun>(Testing);
f();
}
三 总结
在C++一般更推荐新加的static_cast。const_cast,dynamic_cast和reinterpret_cast转化方式;
感谢,Thanks!
版权声明:本文博主原创文章,博客,未经同意不得转载。
C++ 对象模型具体评论(特别easy理解力)的更多相关文章
- zencart批量评论插件Easy Populate CSV add reviews使用教程
此插件在Easy Populate CSV 1.2.5.7b产品批量插件基础上开发,有1.3x与1.5x两个版本. zencart批量评论插件Easy Populate CSV add reviews ...
- so文件成品评论【整理】
这是我的 @布加迪20 AZ在一篇文章中写道:<汉化so文件的心得>中的技术附件做的简洁性整理.原来的看起来不是非常方便.一起分享学习.. 正文 SO文件汉化心得 --By布加迪20 ...
- Meet Apache Wicket
第一次接触Wicket,如此多的内容是文字,的原贴,希望大家指正 Meet Apache Wicket By JonathanLocke, original author of Wicket 乔纳森· ...
- NoSQL数据库介绍(2)
2 NoSQL潮流 在这一章中,将一起讨论NoSQL潮流的动机和主要驱动力.以及NoSQL主张的批评和反馈.本章将通过不同的尝试得出结论来分类和描写叙述NoSQL数据库.当中一个分类法将在随 ...
- zkw线段树详解
转载自:http://blog.csdn.net/qq_18455665/article/details/50989113 前言 首先说说出处: 清华大学 张昆玮(zkw) - ppt <统计的 ...
- codeforces C. Cd and pwd commands 执行命令行
执行命令来改变路径 cd 并显示路径命令 pwd 一个节目的 抽样: input 7 pwd cd /home/vasya pwd cd .. pwd cd vasya/../petya pwd ou ...
- 十分钟学会写shell脚本
大家好!我是handsomecui,下面我为大家讲解一下shell脚本的写法,讲的不好的地方,欢迎大家留言拍砖. 1.在linux下会写shell脚本是非常重要的,下面我参照例子给大家展示几个脚本,顺 ...
- nginx+tomcat负载均衡策略
測试环境均为本地,測试软件为: nginx-1.6.0,apache-tomcat-7.0.42-1.apache-tomcat-7.0.42-2.apache-tomcat-7.0.42-3 利用n ...
- HTML学习_01
html总结 html是一门标记语言,也就是不经过编译就能直接执行的语言,不像是c/c++/java等等须要转换成二进制码, html是一门最主要的学科,提供了一个框架,提供了各种标签和规则,使得语言 ...
随机推荐
- hdu4858 项目管理 bestcoder round1 B
唔..弱弱的暴力水果 0操作时,将v加到u上,能够直接把v加到u相连的点上,这样输出时直接输出要求点的值. 布这种话反正我是超时了.. #include<cstdio> #include& ...
- grep在一个特定的文件搜索文件夹keyword
grep -R --include="*.*"(文件名匹配) key(keyword) dir(夹) eg.在当前文件夹搜索xml关键文件172.19.32.22 grep -R ...
- ASP.NET之AdRotator实现淘宝浏览页面的商品随机推荐功能
如今随便上个网都能够看到淘宝.京东等各大电商平台的双十一购物狂欢宣传,从2009年開始淘宝愣是把11.11这一天打造成了全民购物狂欢节.阿里巴巴的上市更是激发了阿里人的斗志,据说他们今年的目标是100 ...
- LINQ之路(1):LINQ基础
本文将从什么是LINQ(What).为什么使用LINQ(Why)以及如何使用LINQ(How)三个方面来进行说明. 1.什么是LINQ LINQ(Language Integrated Query)是 ...
- HDU 5059 Help him(细节)
HDU 5059 Help him 题目链接 直接用字符串去比較就可以,先推断原数字正确不对,然后写一个推断函数,注意细节,然后注意判掉空串情况 代码: #include <cstdio> ...
- android 实现悬架控制
实现桌面View 如桌面歌词 1)将要显示在桌面的view,通过WindowManager.addView.挂在到WindowManager下;注意,WindowManager对象 ...
- C++四种类型的转换
在C/C++使用的语言 (type) value(您还可以使用type(value))对于显式类型转换,经常提到投.转换程序猿的精度等完全掌握手,一个传统投往往是过度使用.成为C++要根源. 为了降低 ...
- SQL Server错误代码及解释(留着备用)
原文:SQL Server错误代码及解释(留着备用) 转自:http://www.ajia.me/Article/193.html Code Error Message 0 操作成功完成. 1 功能 ...
- ehCache浅谈(转)
ehcache FAQ中提到 Remember that a value in a cache element is globally accessible from multiple threads ...
- UVA714- Copying Books(最大最小化)
意甲冠军:k手稿的部分成m部分,使每一个和最小 思路:典型最大值最小化问题,使用贪心+二分. 贪心的是每次尽量将元素往右边划分,二分查找最小的x满足m个连续的子序列和S(i)都不超过x. 由于输出的原 ...