——new和malloc
~malloc
/* 函数原型 其中__size是你要分配的大小,其单位是byte */ void* malloc(size_t __size); // 用例 int* pInt = (int*) malloc(sizeof(int)); // 分配了一个int double* pDoubleArray = (double*) malloc(sizeof(double) * 5); // 分配了一个double数组,其大小为5
一般来说,malloc总是能为你分配出内存。但是也存在山穷水尽,内存不够用的情况。这时候malloc会返回一个空指针(NULL, nullptr)。当你使用malloc的时候,你最好每次都要判断一下返回的指针是否为空。
现在内存已经分配出来了。当程序运行到某一些时刻,我又不想要这些内存了。这时候我们就要手动释放内存,否则的话就会造成内存泄露。通过free() 函数来释放内存,其函数原型和使用例子如下:
// 原型 void free(void* __ptr); // 用例 free(pInt); free(pDoubleArray);
有趣的是你传给free函数的只是一个指针,然而不管你是分配了一个元素还是分配了一个数组,free总能帮你释放掉这些内存(free 怎么知道你分配的数组的大小?)
详细的说说malloc在分配内存的时候干了什么。malloc在分配内存的时候,不仅仅会分配你需要的内存大小,它还会在你的内存的头部和尾部加上一些额外的信息(俗称cookie)。比如说用来DEBUG的信息和你的内存的大小。这就解释了为什么能free掉你的内存,因为它知道你这块内存是多大的。值得一提的是这些Cookie会占用一些内存。。。
~new
new是c++提供的一个操纵符(或者说关键字)。其也是用于分配内存的。其用例如下:
int* pInt = new int(3); // 分配并构造一个int double* pDoubleArray = new double[5]; // 分配了一个double数组,其大小是5 delete pInt; // 删除单元素 delete[] pDoubleArray; // 删除数组般来说程序是能为你分配出内存的,但是实在山穷水尽了怎么办?这时候程序就会抛出一个std::bad_alloc异常。注意,这是new 和 malloc 的区别之一。但是难能可贵的是C++提供了一个机制来处理bad_alloc异常,其做法如下:
void BadAllocateHandler() { std::cout << "啊,内存分配失败了" << std::endl; exit(0); } std::set_new_handler(BadAllocateHandler);BadAllocateHandler是程序员自己写的当分配失败时的处理函数。而set_new_handler是c++提供的机制。一般来说当山穷水尽的时候你所应该做的事只有两件。要么让程序退出,要么想办法从别的地方挖一些内存来继续分配。
虽然new的本质是去调用malloc,但是new 和 malloc还有一点很大的不同。那就是new 出来内存后,new会帮你把对象给构造出来,而malloc只是分配内存。具体例子如下:
class MyObj { public: public MyObj(int value) : val(value) {} int val; }; MyObj* obj = new MyObj(4); MyObj* obj1 = (MyObj*) malloc(sizeof(MyObj));new 的做法是在malloc分配出内存后,编译器会直接调用类的构造函数在这片内存中构造出对象。注意!只有编译器能直接调用类的构造函数。而你使用malloc的话,是无法直接在上面构造出对象的。
——new和malloc的区别
0. 属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
1. 参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
2. 返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
3. 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
4. 自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
5. 重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
6. 内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
1)申请的内存所在位置
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。new (place_address) type
void * operator new (size_t,void *) //不允许重定义这个版本的operator new
2)返回类型安全性
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图分配自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了
3)内存分配失败时的返回值
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功:
int *a = (int *)malloc ( sizeof (int )); if(NULL == a) { ... } else { ... }
try { int *a = new int(); } catch (bad_alloc) { ... }
4)是否需要指定内存大小
1 class A{...} 2 A * ptr = new A; 3 A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);
5)是否调用构造函数/析构函数
使用new操作符来分配对象内存时会经历三个步骤:
第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
使用delete操作符来释放对象内存时会经历两个步骤:
第一步:调用对象的析构函数。
第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。
class A { public: A() :a(1), b(1.11){} private: int a; double b; }; int main() { A * ptr = (A*)malloc(sizeof(A)); return 0; }在return处设置断点,观看ptr所指内存的内容:
1 int main() 2 { 3 A * ptr = new A; 4 }查看程序生成的汇编代码可以发现,A的默认构造函数被调用了:
6)对数组的处理
A * ptr = new A[10];//分配10个A对象
delete [] ptr;
int * ptr = (int *) malloc( sizeof(int)* 10 );//分配一个10个int元素的数组
7)new与malloc是否可以相互调用
void * operator new (sieze_t size) { if(void * mem = malloc(size) return mem; else throw bad_alloc(); } void operator delete(void *mem) noexcept { free(mem); }
8)是否可以被重载
//这些版本可能抛出异常 void * operator new(size_t); void * operator new[](size_t); void * operator delete (void * )noexcept; void * operator delete[](void *0)noexcept; //这些版本承诺不抛出异常 void * operator new(size_t ,nothrow_t&) noexcept; void * operator new[](size_t, nothrow_t& ); void * operator delete (void *,nothrow_t& )noexcept; void * operator delete[](void *0,nothrow_t& )noexcept;
我们可以自定义上面函数版本中的任意一个,前提是自定义版本必须位于全局作用域或者类作用域中。太细节的东西不在这里讲述,总之,我们知道我们有足够的自由去重载operator new /operator delete ,以决定我们的new与delete如何为对象分配内存,如何回收对象。
9)能够直观地重新分配内存
使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。
10)客户处理内存分配不足
1 namespace std 2 { 3 typedef void (*new_handler)(); 4 }
1 namespace std 2 { 3 new_handler set_new_handler(new_handler p ) throw(); 4 }
set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。
对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。
~总结
——const
~在类成员函数后加const
class test { public: test(int a=10):aa(a){} ~test(){} int getA() const {aa++,bb++;return aa} private: int aa; mutable int bb; }; int main() { test t(100); int cc = t.getA(); };
~const限定类型判断
先从一级指针说起:
(1)const char p 限定变量p为只读。这样如p=2这样的赋值操作就是错误的。
(2)const char *p p为一个指向char类型的指针,const只限定p指向的对象为只读。这样,p=&a或 p++等操作都是合法的,但如*p=4这样的操作就错了,因为企图改写这个已经被限定为只读属性的对象。
(3)char *const p 限定此指针为只读,这样p=&a或 p++等操作都是不合法的。而*p=3这样的操作合法,因为并没有限定其最终对象为只读。
(4)const char *const p 两者皆限定为只读,不能改写。
(1)const char **p p为一个指向指针的指针,const限定其最终对象为只读,显然这最终对象也是为char类型的变量。故像**p=3这样的赋值是错误的,而像*p=? p++这样的操作合法。
(2)const char * const *p 限定最终对象和 p指向的指针为只读。这样 *p=?的操作也是错的。
(3)const char * const * const p 全部限定为只读,都不可以改写。
——sizeof()计算类的大小
sizeof一个空类
class A { }; cout<<sizeof(A)<<endl;//1
注:class A是一个空类型,它的实例不包含任何信息,本来求sizeof应该是0。
但当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。
sizeof一个带有析构和构造函数的类
class B { public: B() {} ~B() {} }; cout<<szieof(B)<<endl;//1
注:class B在class A的基础上添加了构造函数和析构函数。
由于构造函数和析构函数的调用与类型的实例无关(调用它们只需要知道函数地址即可),在它的实例中不需要增加任何信息。
所以sizeof(B)和sizeof(A)一样,在Visual Studio 2008中都是1。
sizeof一个带有虚函数的类
class C { public: C() {} virtual ~C() {} }; cout<<sizeof(C)<<endl;//4
注:class C在class B的基础上把析构函数标注为虚拟函数。C++的编译器一旦发现一个类型中有虚拟函数,
就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。
在32位的机器上,一个指针占4个字节的空间,因此sizeof(C)是4。
C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1。
如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable,
在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。而虚函数本身和其他成员函数一样,是不占用对象的空间的。
sizeof一个带有成员变量的类
class D { char ch; void func() { } }; class E { char ch1; //占用1字节 char ch2; //占用1字节 virtual void func() { } }; class F { int in; virtual void func() { } }; cout << "D的大小"<< sizeof(D) << endl;//1 cout << "E的大小" << sizeof(E) << endl;//8 cout << "F的大小" << sizeof(E) << endl;//8
——register 关键字
register 这个关键字请求编译器尽可能的将变量存在CPU内部寄存器,而不是通过内存寻址访问(不能用&),以提高效率。注意是尽可能,不是绝对
因为,如果定义了很多register变量,可能会超过CPU的寄存器个数,超过容量。所以只是可能。
数据从内存里拿出来先放到寄存器,然后CPU 再从寄存器里读取数据来处理,处理完后同样把数据通过寄存器存放到内存里,CPU 不直接和内存打交道。
速度!就是因为速度。寄存器其实就是一块一块小的存储空间,只不过其存取速度要比内存快得多。
进水楼台先得月嘛,它离CPU 很近,CPU 一伸手就拿到数据了,比在那么大的一块内存里去寻找某个地址上的数据是不是快多了?
举例:
register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。例如下面的内存块拷贝代码,
#ifdef NOSTRUCTASSIGN memcpy (d, s, l) { register char *d; register char *s; register int i; while (i--) *d++ = *s++; } #endif
但是使用register修饰符有几点限制。
1.register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。
2.因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。
3.早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。比如我们定义一个循环变量,
for(int i; i<100;i++) { i *= i; }
——this和super的区别
~this关键字
(1.)每个类的每个非静态方法(没有被static修饰)都会隐含一个this关键字,它指向调用这个方法的对象;当在方法中使用本类属性时,都会隐含地使用this关键字,当然也可以明确使用。
this可以看成是一个变量,它的值就是当前对象的引用
this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用如果是在同一类中调用另外一个方法,则可以不用写this,直接调用
(2.)为了区分属性和局部变量,可以通过this关键字来调用
- 当类中非静态方法的参数名与类的某个成员变量名相同时,为了避免参数作用范围覆盖了成员变量的作用范围,必须明确使用this关键字来指定
- 如果某个构造方法的第一条语句具有形式this(...),那么这个构造方法将调用本类中的其他构造方法
- 如果某个方法需要传入当前对象,则可以将当前的对象作为参数传递给它
public class pra { private String name; private String sex; private Integer age; public pra(String name, String sex, Integer age) { super(); this.name = name; this.sex = sex; this.age = age; } public pra() { this("by", "女", 15); //调到了有参的构造方法 } }
~super关键字
super代表了父类空间的引用
super的作用:
子父类存在着同名的成员时,在子类中默认时访问子类的成员,可以通过super关键字指定访问父类的成员创建子类对象时,默认会先调用父类无参的构造方法,可以通过super关键字指定调用父类的构造方法
- super可以用来引用直接父类的实例变量。
- super可以用来调用直接父类方法。
- super()可以用于调用直接父类构造函数
class C { String name = "A"; public void work() { System.out.println("A工作!"); } } class B extends C { String name = "B"; public B() { super(); // 调用父类构造方法 } public void work() { System.out.println("B工作!"); } public void pint() { System.out.println(name); System.out.println(super.name); // 调用父类的实例变量 super.work(); // 调用父类的方法 } } public class A { public static void main(String[] args) { B b = new B(); b.pint(); } }
注意事项:
*如果在子类的构造方法上没有指定调用父类的构造方法,java编译器会在子类的构造器里面加上super()语句
* super关键字调用父类的构造函数时,该语句必须要是子类构造函数的第一个语句
this和super的区别
1)代表的事物不同
super代表的是父类空间的引用
this代表的是所属函数的调用者对象
2)使用前提不同
super必须要有继承关系才能使用
this不需要继承关系也能使用
3)调用的构造函数不同
super:调用父类的构造函数
——virtual
1)virtual关键字主要是什么作用?
2)哪些情况下可以使用virtual关键字?
3)virtual函数的效果
- class GrandFather
- {
- public:
- GrandFather() {}
- virtual void fun()
- {
- cout << "GrandFather call function!" << endl;
- }
- };
- class Father : public GrandFather
- {
- public:
- Father() {}
- void fun()
- {
- cout << "Father call function!" << endl;
- }
- };
- class Son : public Father
- {
- public:
- Son() {}
- void fun()
- {
- cout << "Son call function!" << endl;
- }
- };
- void print(GrandFather* father)
- {
- father->fun();
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- Father * pfather = new Son;
- pfather->fun();
- GrandFather * pgfather = new Father;
- print(pgfather);
- return 0;
- }
4)virtual的继承性
5)虚析构函数
class GrandFather { public: GrandFather() {} virtual void fun() { cout << "GrandFather call function!" << endl; } ~GrandFather() { cout << "GrandFather destruction!" << endl; } }; class Father : public GrandFather { public: Father() {} void fun() { cout << "Father call function!" << endl; } ~Father() { cout << "Father destruction!" << endl; } }; class Son : public Father { public: Son() {} void fun() { cout << "Son call function!" << endl; } ~Son() { cout << "Son destruction!" << endl; } }; void print(GrandFather* p) { p->fun(); } int _tmain(int argc, _TCHAR* argv[]) { Father * pfather = new Son; delete pfather; return 0; }输出为
Father destruction! GrandFather destruction!
6)纯虚函数
class GrandFather { public: GrandFather() {} virtual void fun() = 0 { cout << "GrandFather call function!" << endl; } virtual ~GrandFather() { cout << "GrandFather destruction!" << endl; } };
- 纯虚函数为后代类提供可覆盖的接口,但这个类中的版本决不会调用。
- 含有(或继续)一个或多个纯虚函数的类是抽象基类,抽象基类不能实例化!
- 继承类只有重写这个接口才能被实例化
7)虚继承
class GrandFather { public: GrandFather() {} void fun() { cout << "GrandFather call function!" << endl; } virtual ~GrandFather() { cout << "GrandFather destruction!" << endl; } }; class Father1 : public GrandFather { public: Father1() {} void fun() { cout << "Father call function!" << endl; } }; class Father2 : public GrandFather { public: Father2() {} void fun() { cout << "Father call function!" << endl; } }; class Son : public Father1, public Father2 { public: Son() {} //void fun() //{ // cout << "Son call function!" << endl; //} }; void print(GrandFather* p) { p->fun(); } int _tmain(int argc, _TCHAR* argv[]) { Son* son = new Son; son->fun(); return 0; }
8) 构造函数和析构函数中的虚函数
9)虚函数实现机制
10)小结
class classA { public: classA() { clear(); } virtual ~classA() { } void clear() { memset(this , 0 , sizeof(*this)); } virtual void func() { printf("func\n"); } }; class classB : public classA { }; int main(void) { classA oa; classB ob; classA * pa0 = &oa; classA * pa1 = &ob; classB * pb = &ob; oa.func(); // 1 ob.func(); // 2 pa0->func(); // 3 pa1->func(); // 4 pb->func(); // 5 return 0; }
memset(this , 0 , sizeof(*this));
classB()
{
clear();
}
virtual ~classB()
{
}
void clear()
{
memset(this , 0 , sizeof(*this)); }
——explicit关键字
- 只可以修饰类的构造函数,一般的函数不能修饰。
- 只有类的构造函数有一个参数时候,才有效,当有多个参数时候就没什么意义了(这里有个例外,就是当除了第一个参数以外的其他参数都有默认值的时候,explicit依然有效,此时调用构造函数时只传入一个参数,等效于只有一个参数的类构造函数)。
class MyClass //没有使用explicit关键词,默认为隐式声明 { public: int m_size; char *m_pStr; MyClass(int size) //没有使用explicit关键词,默认为隐式声明 { m_size = size; //大小 m_pStr = (char *)malloc(size + 1); //给m_pstr 分配size的大小的内存 memset(m_pStr, 0, size + 1); } MyClass(const char *pStr) //没有使用explicit关键词,默认为隐式声明 { m_size = strlen(pStr); m_pStr = (char *)malloc(m_size + 1); //给m_pstr分配内存 strcpy(m_pStr, pStr); //复制内容 } MyClass(int size, const char *pStr) //参数为两个,所以不管是否使用explicit关键字,都是无效的。 { m_size = size; int size1 = strlen(pStr); //大小 m_pStr = (char *)malloc(size1 + 1); //给m_pstr分配内存 strcpy(m_pStr, pStr); } };下面是调用:
MyClass c1(10); // ok, 显式调用 MyClass(int size) MyClass c2 = 12; // ok, 隐式调用 MyClass(int size) MyClass c3; // 错误, 没有响应的构造函数 MyClass c4("hello"); // ok, 显式调用 MyClass(const char *pStr) MyClass c5 = "nihao"; // ok, 隐式调用 MyClass c6(5, "hello"); // ok, 显式调用 MyClass(int size, const char *pStr) c1 = 5; // ok, 隐式调用 MyClass(int size) c2 = 6; // ok, 隐式调用 MyClass(int size) c4 = "hello world"; // ok, 隐式调用 MyClass(const char *pStr) c3 = c1; // 错误, 编译是没问题的,但是必须在类里重载“=”运算符才行
因为在C++中,如果构造函只有一个参数时,那么编译的时候就会有一个缺省的转换操作:将该构造函数对应的数据类型的数据转换为该类对象。
使用关键字explicit
class MyClass { public: int m_size; char *m_pStr; explicit MyClass(int size) { m_size = size; //大小 m_pStr = (char *)malloc(size + 1); //给m_pstr 分配size的大小的内存 memset(m_pStr, 0, size + 1); } explicit MyClass(const char *pStr) { m_size = strlen(pStr); m_pStr = (char *)malloc(m_size + 1); //给m_pstr分配内存 strcpy(m_pStr, pStr); //复制内容 } explicit MyClass(int size, const char *pStr) //两个参数,所以添加explicit是无效的。 { m_size = size; int size1 = strlen(pStr); //大小 m_pStr = (char *)malloc(size1 + 1); //给m_pstr分配内存 strcpy(m_pStr, pStr); } };再次编译一下结果如下:
上面也说过了,explicit关键字只对有一个参数的类构造函数有效,如果函数的参数大于或者等于两个时候,是不会产生隐式转换的,所以explicit也就是无效的。
explicit MyClass(int size, const char *pStr = "hello") { m_size = size; int size1 = strlen(pStr); //大小 m_pStr = (char *)malloc(size1 + 1); //给m_pstr分配内存 strcpy(m_pStr, pStr); }