C++基础知识点整理

2020-07-22  本文已影响0人  异同
  1. 使用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();
即在赋值前已经使用指针符号(*)获取到了实例对象

  1. 堆内存、栈内存、内存释放和野指针的问题

代码报错内容:
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的处理的。否则在后续代码中可能无意识的仍然在使用该指针处理数据,造成数据出现篡改的情况。

  1. 友元函数
    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在前面,运算符在中间,第二个参数在最后面。

  1. 运算符重载
    定义方法:
    返回值类型 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;
}
上一篇下一篇

猜你喜欢

热点阅读