C++基础知识点整理
- 使用new实例化出来的对象会放在堆区,一般用于复杂数据类型的实例化操作。这种方式实例化后不会自动释放空间,要使用
delete
进行手动释放,以避免内存泄露。
直接实例化出来的对象会放在栈去,一般存放结构简单且空间占用较小的数据类型。使用该方式实例化后会自动释放空间。
注意
使用类似new进行实例化,返回的是指向这个对象的指针而不是这个对象本身。因此使用
MyCalendar cal=new MyCalendar();
会报错"no viable conversion from 'MyCalendar *' to 'MyCalendar'",即无法将一个MyCalendar对象的指针赋值给一个MyCalendar对象。此时应当使用MyCalendar *cal=new MyCalendar();
。
另外要注意的是,变量是对象的时候用“.”访问,而变量是对象指针的时候用“->”访问。
例:
方式1:
MyCalendar *cal=new MyCalendar();
cal->print_cur_date();
delete cal;
方式2
MyCalendar cal= MyCalendar();
cal.print_cur_date();
等同于:
MyCalendar cal=*new MyCalendar();
cal.print_cur_date();
即在赋值前已经使用指针符号(*)获取到了实例对象
- 堆内存、栈内存、内存释放和野指针的问题
代码报错内容:
HeapSortV2(9693,0x1098dedc0) malloc: *** error for object 0x7ffeea0a77b0: pointer being freed was not allocated
HeapSortV2(9693,0x1098dedc0) malloc: *** set a breakpoint in malloc_error_break to debug
错误情况1
int data[] = {1,2,3,4,5};
int *v = data
delete [] v;
v = NULL;
错误情况2
int data[] = {1,2,3,4,5};
int *v = new int[5];
v = data;
delete [] v;
v = NULL;
错误解释:
首先要明白的是c++内存分配一般包括分配在堆内存和栈内存两种情况。
栈(stack)内存基本上都是系统自动分配的,例如示例中的int data[] = {1,2,3,4,5};
,即常规的变量(包括数组)赋值操作,都是在堆上进行的。
堆(heap)内存多为用户自定义分配的,例如通过malloc申请内存,例如通过new关键字创建的实例变量。
简单来说:栈内存由系统控制自动分配和释放内存空间,限制性较大,适用于生命周期短的变量、函数参数,一般分配的时候都是直接分配一整片连续的内存。而堆内存由程序员自己控制内存的分配和释放,灵活性强,但是由于它内存分布不是连续的,会涉及到寻址的问题,因此速度比栈内存要慢一些。而且new或malloc出来的内存如果不做释放,可能会造成内存泄露(即某些内存被占用而未做释放,导致内存资源浪费)的问题。
通过free()或delete、delete[]等释放内存,只能用于堆内存数据。而且其实根据上面的叙述也能知道,栈内存的数据是不需要做资源释放的,系统会自行释放栈内存数据。
那么错误情况1就能解释了
我们创建了一个数组int data[] = {1,2,3,4,5};
,这个数组是在栈内存的,会自行释放。我们用指针v指向了这个数组int *v = data
,这时依然没有新开辟的空间,指针v只是在调用栈中创建的一个int指针类型的数据,他的内容是一个指向int类型数据的内存地址的值。
那么可以看到,我们从来没有在堆内存中申请内存空间,因此delete操作是没有意义的。提示的“pointer being freed was not allocated”,意思是指针指向的那块要被清空的(堆)内存是没有被分配过数据的,说白了就是这个指针指向的是栈内存中的数组,没有指向一个堆内存,又哪里去谈什么delete/free内存呢。
修改为如下即可,即直接让系统进行内存的释放,不需要手动干预。
int data[] = {1,2,3,4,5};
int *v = data
情况2
根据我们之前说的,free和delete是一定要在new出来或则是malloc出来的数据上进行的,在情况2中我们首先new了一个数组,并让指针v指向这个数组int *v = new int[5];
,这时候如果我们使用delete[] v
会发现是可以正常运行的,但是如果我们想情况2中又为指针v重新赋值,使他指向了位于栈中的int类型数组,那么实际上效果是和情况1等同的,这是v并没有指向一个堆内存数据,使用delete/free自然就会出错了。
实际上当我们将代码改为以下内容后,还需要再添加一个对指针v的清空操作,否则当v指向的堆内存清空后,v指向了一个没有实际有效数据的内存区域,成了一个野指针。我们可以添加v = NULL;
,或者是另v指向其他内容。
int *v = new int[5];
delete [] v;
综合学到的内容,我们做一个实验:
template <typename Item>
class MaxHeap{
private:
Item *data;
public:
MaxHeap(Item data[]){
// 直接传入一个数组,对该数组执行heapify,构建为堆
this->data = data;
__heapify();
}
~MaxHeap(){
delete[] data;
}
}
int main() {
int data[] = {1,2,3,4,5};
MaxHeap<int> maxHeap = MaxHeap<int>(data);
}
上述代码会报同样的错误。分析内容可以看到,我们将创建在栈上的数组data作为构造函数的参数进行实例化,在实例化中我们让成员变量data(指针)指向了数组data的第一个元素,然后执行heapify操作。在执行完毕后我们在稀构函数中使用了delete,即犯了同上面一样的毛病,对并不是new/malloc出来的数据进行了delete/free操作。
一个解决办法是我们new一片空间出来,然后遍历data数组的内容,挨个进行赋值:
template <typename Item>
class MaxHeap{
private:
Item *data;
public:
MaxHeap(Item data[], int n){
// 直接传入一个数组,对该数组执行heapify,构建为堆
this->data = new Item[n]
for(int i = 0; i < n ; i++){
this->data[i] = data[i]
}
__heapify();
}
~MaxHeap(){
delete[] data;
data = NULL:
}
}
int main() {
int data[] = {1,2,3,4,5};
MaxHeap<int> maxHeap = MaxHeap<int>(data,5);
}
当然我们可以直接使用指针对原数组进行操作,而不必再开辟新的内存空间(一般不建议这么处理)。这时我们的稀构函数其实只需要根据规范标准,把野指针处理掉就好了。
template <typename Item>
class MaxHeap{
private:
Item *data;
public:
MaxHeap(Item data[]){
// 直接传入一个数组,对该数组执行heapify,构建为堆
this->data = data
__heapify();
}
~MaxHeap(){
data = NULL:
}
}
int main() {
int data[] = {1,2,3,4,5};
MaxHeap<int> maxHeap = MaxHeap<int>(data,5);
}
要注意的是,不管指针指向了malloc/new的堆内存空间,还是指向了在栈内存中的普通数据类型及其数组,通过free、delete或者是系统自动回收机制将内容清空后,这个指针都是指向了无效数据,按照规范而言是一定要做重新指向或者赋值为NULL的处理的。否则在后续代码中可能无意识的仍然在使用该指针处理数据,造成数据出现篡改的情况。
- 友元函数
c++类中有public和private两种成员变量及方法,如果我们在外部实例化了一个类,那么我们是无法访问到这个类的私有成员变量及私有方法的。
如:
#include <iostream>
using namespace std;
class A{
private:
int a=1;
int b=2;
void printPrivate(){
cout<<"this is a private function"<<endl;
}
public:
int c =3;
int d=4;
void printPublic(){
cout<<"this is a public function"<<endl;
}
};
//
int main(){
A classA = A();
//这时我们无法使用A对象的私有方法printPrivate、私有成员变量a及私有成员变量b
//error: 'a' is a private member of 'A'
cout<<classA.a<<endl;
//error: 'b' is a private member of 'A'
cout<<classA.b<<endl;
//error: 'printPrivate' is a private member of 'A'
classA.printPrivate();
return 0;
}
友元函数可以解决这种无法访问私有成员变量及私有方法的问题。
可以这么理解,友元就好比是类是一个特殊的成员,它不是类所拥有的,但是又能访问类的数据,可以假想是这个类的一个“朋友(friend)”。
这个函数的使用方法可以有多种,用的最多的是:
在该类的内部进行声明,在该类的外部进行定义
#include <iostream>
using namespace std;
class A{
private:
int a=1;
int b=2;
void printPrivate(){
cout<<"this is a private function"<<endl;
}
public:
int c =3;
int d=4;
void printPublic(){
cout<<"this is a public function"<<endl;
}
//友元函数要在需要开放私有数据的那个类的内部进行声明
//声明方法就是friend+返回值类型+函数名(参数列表)。
//一般参数列表中可以包含这个类的本身
friend void access_1(A);
//也可以不包含这个类本身
friend void access_2();
};
//友元函数要在类外部进行定义
void access_1(A classA){
cout<<"access_1 running"<<endl;
cout<<classA.a<<endl;
cout<<classA.b<<endl;
classA.printPrivate();
}
//友元函数要在类外部进行定义
void access_2(){
cout<<"access_2 running"<<endl;
A classA = A();
cout<<classA.a<<endl;
cout<<classA.b<<endl;
classA.printPrivate();
}
int main(){
A classA = A();
access_1(classA);
access_2();
return 0;
}
------------------执行结果如下-------------------
access_1 running
1
2
this is a private function
access_2 running
1
2
this is a private function
除了友元函数以外,还有友元类,即friend class XXX。使用方法类似,我们在A类中声明friend class B,然后在A类外部对B类进行具体的定义,那么在B类中我们就能访问到A类(对象)的所有数据了:
#include <iostream>
using namespace std;
class A{
private:
int a=1;
int b=2;
void printPrivate(){
cout<<"this is a private function"<<endl;
}
public:
int c =3;
int d=4;
void printPublic(){
cout<<"this is a public function"<<endl;
}
//声明友元类B
friend class B;
};
//定义友元类B
class B{
public:
void accessToClassA(A classA){
cout<<classA.a<<endl;
cout<<classA.b<<endl;
classA.printPrivate();
}
void accessToClassA(){
A classA = A();
cout<<classA.a<<endl;
cout<<classA.b<<endl;
classA.printPrivate();
}
};
int main(){
A classA = A();
B b = B();
b.accessToClassA();
b.accessToClassA(classA);
return 0;
}
------------------执行结果如下-------------------
1
2
this is a private function
1
2
this is a private function
还有一种使用方法,是把友元函数应用在多个类上,例如对两个不同类的某些个私有成员变量进行操作:
#include <iostream>
using namespace std;
//如果下面友元方法参数列表中出现了两个类型,如B类型,
//则必须在前面进行声明(专业术语为前序声明:forward declaration)
class B;
class A{
private:
int a0;
public:
A(int a0){
this->a0 = a0;
}
//我们在A、B类外定义的sumAB方法要用到A和B两个类的数据,
//因此在A和B两个类中都要进行友元函数的声明
friend int sumAB(A,B);
};
class B{
private:
int b0;
public:
B(int b0){
this->b0 = b0;
}
//我们在A、B类外定义的sumAB方法要用到A和B两个类的数据,
//因此在A和B两个类中都要进行友元函数的声明
friend int sumAB(A,B);
};
//在外部定义友元函数
int sumAB(A classA,B classB){
return classA.a0+classB.b0;
}
int main(){
A classA = A(123);
B classB = B(456);
cout<<sumAB(classA,classB);
}
------------------执行结果如下-------------------
579
当然,我们也可以在友元函数的参数列表中不加入AB两个类,而是在这个函数的定义中进行AB类的实例化处理。也就是说并不是说友元函数的参数列表中有类A类B所以我们能访问他们的私有方法,而是因为我们在类中声明了友元函数,所以我们能够通过这个友元函数来访问类的私有方法私有成员变量,这个不要搞混了。
#include <iostream>
using namespace std;
class A{
private:
int a0;
public:
A(int a0){
this->a0 = a0;
}
friend int sumAB();
};
class B{
private:
int b0;
public:
B(int b0){
this->b0 = b0;
}
friend int sumAB();
};
//我们不指定友元函数的参数列表内容,而在函数的定义中进行对象实例化
//但是一定要满足定义时的参数列表和声明时的参数列表是一致的,
//因为C++重载的特性,参数列表不一致的话会认为是两个函数
int sumAB(){
A classA = A(123);
B classB = B(456);
return classA.a0+classB.b0;
}
int main(){
cout<<sumAB();
}
------------------执行结果如下-------------------
579
我们需要明确一个观点:友元函数的定义是可以出现在类内部的,也就是说我们可以在类的内部既声明友元函数,又定义友元函数,而不一定是"类的内部声明友元函数,类的外部定义友元函数"。"类的内部声明友元函数,类的外部定义友元函数"只是一种最常见的用法,而不是唯一使用方法。
但是要注意C++不允许在类内部定义友元函数,在外部再次定义友元函数。这可不像java的什么可以对一个函数进行重写复用。友元函数的定义只能出现在一个位置。
#include <math.h>
#include <iostream>
using namespace std;
class A{
private:
double x;
double sqrt_x;
public:
A(int x){
this->x = x;
sqrt_x = sqrt(x);
}
friend void printData(A a){
cout<<"x = "<<a.x<<", sqrt x = "<<a.sqrt_x<<endl;
}
};
int main(){
A a = A(1024);
printData(a);
return 0;
}
4.在类中对 <<运算符的重载
我们看一段代码:
#include<math.h>
#include <iostream>
using namespace std;
class A{
private:
double x;
double sqrt_x;
public:
A(int x){
this->x = x;
sqrt_x = sqrt(x);
}
friend ostream& operator<<(ostream& os,A a){
os<<"x = "<<a.x<<", sqrt_x = "<<a.sqrt_x<<endl;
}
};
int main(){
A a = A(1024);
cout<<a;
return 0;
}
------------------执行结果如下-------------------
x = 1024, sqrt_x = 32
这是一个很简单的<<运算符重载,如果按照常规运算符重载的语法规则,我们只需要写成ostream& operator<<(ostream& os,A a)就行了,为什么要加friend修饰呢?
ostream&:(返回值类型)
operator:(固定内容)
<<:(需要重载的运算符)
(ostream& os,A a):(参数列表){
这是因为在类中定义的运算符重载,在使用及书写的时候一定是类对象在运算符前面的,这样一来我们要使用<<的时候就应当写成a<<cout
而不是我们所习惯的cout<<a
。
也就是说,我们在前面加friend只是为了把他当做一个普通函数方法,而不是把他当做成员方法(因为成员方法的调用是一定要将类对象写在前面的),那么在使用的时候就能按照普通函数的使用方法,按参数列表的顺序进行使用,即os(类型ostream,其标准实例对象为cout)的实例对象cout在前面,运算符在中间,第二个参数在最后面。
- 运算符重载
定义方法:
返回值类型 operator需要重载的运算符(operator和运算符中间没有任何符号)(参数列表)
如:
#include <iostream>
using namespace std;
class A{
private:
int x;
double y;
public:
A(int x) {
this->x = x;
y = -x;
}
//定义两个不同的A对象中,较大的是它成员变量y较大的哪一个
bool operator>(A anotherObjectA){
return this->y > anotherObjectA.y;
}
};
int main(){
A a1 = A(123);
A a2 = A(234);
cout<<(a1>a2)<<endl;
}