类和对象——封装和对象特性(C++)

C++与C不同,C是面向过程编程,在进行编程时,首先需要考虑构建一个过程,对输入进行处理得到输出。C++是面向对象编程,在进行编程时,首先需要考虑构建一个对象,让这个对象模型配合相对应的问题,通过获取对象的信息得到输出。

对象:将数据和对数据的操作放在一块,将其作为一个相互依靠的整体,称之为对象。

对同类对象的特性抽象出来,类中的绝大多数数据只能通过类的方法进行操作。

C++面向对象的三大特性:封装、继承、多态。

封装的意义:

将一事物的属性和行为抽象出来作为一个对象,将其属性私有化,行为公开化,提高了数据的隐蔽性,隐蔽了类的实现细节和成员数据,实现了代码模块化,提高了复用性。

Class A{
    //属性
    //行为
};

类中的属性和行为统称为成员。属性:成员属性、成员变量。行为:成员函数、成员方法。

类在进行设计时,可以把属性和行为放在不同的权限下,加以控制。一共有三个访问权限:

1.public公共权限,类内可以访问,类外也可以访问。

2.protected保护权限,类内可以访问,类外不可以访问。

3.private私有权限,类内可以访问,类外不可以访问。

对象的初始化和清理:

对象的初始化和清理是两个非常重要的安全问题。一个对象或者变量没有初始化,对其使用的后果是未知的。同样的使用完一个对象或者变量,没有进行及时清理,也会造成一定的安全问题。

C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对对象的初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事,因此如果我们没有提供构造函数和析构函数,编译器会提供构造函数和析构函数(构造函数和析构函数都是空实现)。

构造函数和析构函数:

构造函数:主要作用是在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。

析构函数:主要作用是对象在销毁前系统自动调用,执行一些清理工作。

构造函数的语法:

1.构造函数,没有返回值,不写void。

2.函数名称与类名相同。

3.构造函数可以有参数,因此可以发生重载。

4.程序在调用对象时候会自动调用程序,无需手动调用,而且只会调用一次。

Class A{
    A(){
        
    }
};

析构函数的语法:

1.析构函数,没有返回值,不写void。

2.函数名称与类名相同,在名称前加上符号~。

3.析构函数不可以有参数,因此不可以发生重载。

4.程序在对象销毁前自动调用析构,无需手动调用而且只会调用一次。

Class A{
    ~A(){
          
    }
};

注意:构造函数和析构函数都是必须有实现的,如果不提供,编译器会提供一个空实现的构造和析构函数。

构造函数的分类以及调用:

两种分类方式:

按参数方式:有参构造和无参构造(默认构造函数)。

按类型分类:普通构造和拷贝构造。

三种调用方式:

1.括号法。

2.显示法。

3.隐式转换法。

class person{
public:
       int val;
public:
       person(){
           cout<<"无参构造" <<endl;       
       }
       person(int a){
           cout<<"有参构造" <<endl;      
       }
       person(const person &p){
           cout<<"拷贝构造" <<endl;       
       }
       ~person(){
           cout<<"析构函数" <<endl;       
       }               
}

void test01(){
    括号法:
    person p1;//调用默认构造函数(无参构造)
    person p2(10);//调用有参构造函数
    person p3(p2);//调用拷贝构造函数
    注意:调用默认构造函数时不要加(),如:person p1();
    因为编译器会认为person p1();是一个函数声明,不会认为在创建对象
    
    显示法:
    person p1;//无参构造
    person p2=person(10);//有参构造
    person p3=person(p2);//拷贝构造
    
    person(10):匿名对象
    匿名对象特点:当前行执行结束,系统会立即回收掉匿名对象
    
    注意:不要利用拷贝函数初始化匿名对象
    person(p3);//编译器会认为person (p3)=person p3;认为是声明
    
    
    隐式转换法:
    person p4=10;//相当于写的:person p4=person(10):有参构造
    person p5=p4;//拷贝构造
}

拷贝构造函数调用的时机:

C++中拷贝函数调用时机通常有三种情况:

1.使用一个已经创建完毕的对象来初始化一个新的对象。

#include<iostream>
using namespace std;

class Person {
public:
    int val;
public:
    Person() {
        cout << "无参构造" << endl;
    }
    Person(int a) {
        val = a;
        cout << "有参构造" << endl;
    }
    Person(const Person& p) {
        val = p.val;
        cout << "拷贝函数" << endl;
    }
    ~Person(){
        cout << "析构函数" << endl;
    }
};

void test01() {
    Person p1(10);
    Person p2(p1);
    cout << "p2的val=" << p2.val << endl;
}

int main() {
    test01();
    return 0;
}

2.值传递的方式给函数参数传值。

#include<iostream>
using namespace std;

class Person {
public:
    int val;
public:
    Person() {
        cout << "无参构造" << endl;
    }
    Person(int a) {
        val = a;
        cout << "有参构造" << endl;
    }
    Person(const Person& p) {
        val = p.val;
        cout << "拷贝函数" << endl;
    }
    ~Person(){
        cout << "析构函数" << endl;
    }
};

void doWork(Person p) {

}

void test02() {
    Person p1;
    doWork(p1);
}

int main() {
    test02();
    return 0;
}

3.以值方式返回局部对象。

#include<iostream>
using namespace std;

class Person {
public:
    int val;
public:
    Person() {
        cout << "无参构造" << endl;
    }
    Person(int a) {
        val = a;
        cout << "有参构造" << endl;
    }
    Person(const Person& p) {
        val = p.val;
        cout << "拷贝函数" << endl;
    }
    ~Person(){
        cout << "析构函数" << endl;
    }
};

Person doWork2() {
    Person p2;
    return p2;
}

void test03() {
    Person p1 = doWork2();
}

int main() {
    test03();
    return 0;
}

构造函数的调用规则:

默认情况下,C++编译器至少给一个类添加3个函数:

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,堆属性进行值拷贝

如果用户定义有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝函数。如果用户定义拷贝构造函数,C++不会再提供其他构造函数。

深拷贝和浅拷贝:

浅拷贝:

简单的复制拷贝操作。仅复制对象的成员值(包括指针的地址),不复制指针指向的实际数据。多个对象的指针成员共享同一块内存区域,修改任意一个对象的数据会影响其他对象。

行为:

1.默认的拷贝构造函数和赋值运算符执行浅拷贝。

2.若对象包含指针成员,对个对象可能指向同一块内存。

风险:

1.悬垂指针:一个对象析构后释放内存,其他对象指针失效。

2.双重释放:多个对象析构时重复释放同一块内存,导致程序崩溃。

使用场景:

1.不包含指针成员时,可直接赋值操作。

2.临时共享数据且无需修改原数据时。

Class A{
    int* data;
    A(int value){
        data=new int(value);
    }
    ~A(){
        if(data!=NULL){
            delete data;
            data=NULL;
        }
    }
};

深拷贝:

在堆区重新申请空间,进行拷贝操作。复制指针成员指向的实际数据,而非仅复制地址。为指针成员分配独立内存,并完整赋值原指针指向的数据,确保对象间数据完全隔离。每个对象拥有独立的内存空间,修改操作互不影响。

行为:

1.需要手动实现拷贝构造函数和赋值运算符。

拷贝构造函数必须为指针成员分配内存,并复制数据。

赋值运算符需要先释放原内存再分配新内存,避免内存泄漏。

2.每个对象拥有独立的资源副本,避免共享内存。

使用场景:

1.类包含指针成员其指向动态分配的资源。

2.需要独立修改数据且避免内存冲突时。

Class A{
    int * data;
    A(const A& other){
        data=new int(*other.data);
    }
    A& operator=(const A& other){
        if(this!=&other){
            delete data;
            data=new int(*other.data);
        }
        return *this;
    }
};

浅拷贝和深拷贝的核心区别就是内存管理:

特征浅拷贝深拷贝
内存共享多个指针指向同一块内存每个指针都拥有独立的内存空间
数据独立性修改一个对象的数据会影响其他对象对象间数据完全独立,互不影响
内存风险容易导致双重释放或者悬垂指针内存管理更安全,但是需要手动分配与释放
实现复杂度编译器默认行为,无需手动实现需自定义拷贝构造函数和赋值运算符

初始化列表:

Class A{
    int number;
    int * data;
    A(int a,int b):number(a),data(&b){}
};

上述例子中,number(a)的值传递给构造函数中的第一个参数,data(&b)中的值,传递给构造函数中的第二个参数。

类可以作为其他类的成员:

C++类中成员是另一个类对象,称该成员是对象成员。

Class A{
public:
    A(){}
    A(string Name){
        this->Name=Name;
    }
    ~A(){}
string Name;
};

Class B{
public:
    B(){}
    B(string name,string Name):name(name),Name(Name){}
    ~B(){}
string name;
A Name;
}

void func(){
    B b("A类","B类");
    cout<<b.Name.Name<<b.name;
}

输出的结果是:B类A类

B类中的初始化列表中,Name传递给Name时,相当于A.Name=Name,所以第二个值"B类"就传递给了B类的成员变量A类Name。

总结:

当类作为其他类的成员变量时候,构造和析构应该注意先后顺序。

1.构造顺序:当其他类的对象作为本类的成员,先构造其他类的对象,再构造本身。

2.析构顺序:析构顺序与构造函顺序相反。

静态成员:

静态成员就是在成员变量之前加上关键字static,成为静态成员变量。静态成员只有一份存在于公共内存中。

Class A{
    int a;
    static int b;
};

普通成员变量:每一个类对象中都有一份属于自己的成员变量,相互独立,没有关联。普通成员与对象绑定,随对象的建立而建立,随对象的消亡而消亡,类似于局部变量。

静态成员特点:

1.一个类中的所有静态成员变量只有唯一一份,被所有类共享。

2.成员变量被声明为静态成员变量,就不统计该成员的字节数,静态成员变量在对象中不占用存储空间,只是一个声明,内存与全局变量在一个区域。

3.静态成员变量必须在类内加static声明,在类外定义初始化,定义时不需要加static,不能再构造函数的初始化列表中创建,也不能用初始化列表初始化,如果未初始化,默认为0。

4.静态成员变量的生命周期不依赖于某个类对象,伴随着整个程序的生命周期。

5.静态成员变量不依赖任何类对象(属于类但不属于对象),所以可以不实例化类对象就可以直接访问。

静态成员函数:

1.没有隐藏的this指针,在静态成员函数中,不能直接访问普通成员变量和普通成员函数,可以间接访问,通过手动传递对象的地址。

2.可以直接访问静态成员变量和静态成员函数。

3.修饰静态成员函数,只能在类中,不能再类外,因为外部实体函数前添加static与文件内的作用域功能static冲突。

4.调用方式:

通过对象进行访问或者通过类名进行访问。

class A {
public:
    //静态成员变量也有访问权限
    static int val;
};
int A::val = 100;

void test() {
    A a;
    A b;
    //有两种方式进行访问:
    //1.通过对象进行访问:
    cout << a.val << endl;
    b.val = 200;
    cout << b.val << endl;
    //2.通过类名进行访问:
    cout << A::val << endl;
    //所有对象共享同一份数据,只要其中一个对其值进行改动,所有对象进行访问的值都会改变
}

 静态成员的作用:

1.静态成员相当于多了一层类作用域的全局变量、全局函数。

2.静态成员变量适合存储所有类对象的公共属性。

3.静态成员函数可以当作访问私有的静态成员变量的公开接口,一方面不破坏类的封装性,另一方面可以不需要实例化对象也能调用类的静态成员,让类本身具有管理自己的能力。

类的占用内存空间大小:

C++中,类本身是一个类型定义,不直接占用内存,只有当实例化时,实例化对象才会真正占用内存。所以当调用sizeof(类名),实际上是在询问一个实例化对象的内存大小,而不是类本身的内存大小。

成员变量和成员函数时分开存储的。

非静态成员变量属于类的对象上;静态成员变量不属于类的对象上;静态成员函数不属于类的对象上;非静态成员函数不属于类的对象上。

空类的占用内存空间大小:

//成员变量和成员函数时分开存储的
class A {
public:
    
};//空类

void test() {
    A a;
    cout << "类A的存储空间大小=" << sizeof(a) << endl;
}

因为C++编译器会给每一个空对象分配一个字节的空间,是为了区分空对象占用内存的位置。每个空对象也有一个独一无二的内存地址。

非空类占用内存空间大小:

class A {
public:
    int val;//非静态成员变量,属于类的对象上
    static int num;//静态成员变量,不属于类的对象上
 static void func(){}//静态成员函数,不属于类的对象上
 void func1(){}//非静态成员函数,不属于类的对象上
};

void test() {
    A a;
    cout << "类A的存储空间大小=" << sizeof(a) << endl;
}

 类内部的成员变量:

普通的变量:要占用内存,但是要注意对其原则。

static修饰的静态变量:不占用内存,因为编译器将其放在了全局变量区域。

类内部的成员函数:

普通函数:不占用内存。

虚函数:有一个指向虚函数的指针,要占用4个或者8个字节(根据系统位数来决定),用来指定虚函数的虚拟函数表的入口地址,所以一个类的虚函数所占用的内存地址是不变的,和虚函数的个数无关。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值