C++学习记录(五)——C++类模型初探

类的概念

从概念上来讲:类是现实中具备共同特色食物的抽象。程序员

这里举一个简单的例子:咱们如今须要定义的一我的的类。因此咱们要找出人和人的共同点(或者说人这个物种的共性),人有身高,体重,年龄,性别...这些就是人这个类的属性(成员变量)。人还会吃饭,走路,跳跃...这些就是人的方法(成员函数)。这样就能够抽象出人这个类。编程

类的定义和实现

当咱们抽象出人这个类以后咱们具体要怎么实现这个类呢?
请看以下语句数组

class Person
{
...
};

以上就是类 的基本语法:class 类的名字 {...}; 这里大括号最后有分号。
咱们在大括号里面写成员变量和成员方法。函数

类的成员函数和成员变量

class Person
{
public:
    int age;//人的年龄
    void eat()//人吃饭的成员函数
    {
        std::cout<<"人在吃饭"<<std::endl;
    }
    void run()//人跑步的成员函数
    {
        std::cout<<"人在跑步"<<std::endl;
    }
};

咱们也能够在类外写函数定义,类内只写函数声明。不过这时咱们须要用到做用域标识符(::)性能

class Person
{
public:
    int age;//人的年龄
    bool sex;//人的性别
    void eat();//人吃饭的成员函数
    void run();//人跑步的成员函数
};
void Person::eat()//人吃饭的成员函数
{
    std::cout<<"人在吃饭"<<std::endl;
}
void Person::run()
{
    std::cout<<"人在跑步"<<std::endl;
}

在这里须要强调的是,定义位于类内的函数自动成为内联函数。若是但愿类外定义的函数也能够内联的话,须要手动加inline关键字。优化

类对象的初始化

这里和结构体很像
Person xiaoming;
这里p就是Person的对象,他具备Person的属性和方法。简单的来讲,Person至关于人这个物种,而p这个对象就是具体到谁。
就像结构体那样:this

xiaoming.run();
xiaoming.eat();

在这里所建立的每个对象都有一个本身的存储空间,用于存储本身的成员变量和成员函数。可是多有对象都共享同一个类。debug

类成员的访问权限

这里先简单的了解一下,以后会在继承中详细指出。(友元能够暂时无视)
private: 只能由该类中的函数、其友元函数访问,不能被任何其余访问,该类的对象也不能访问. 指针

protected: 能够被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问 code

public: 能够被该类中的函数、子类的函数、其友元函数访问,也能够由该类的对象访问

注意:这里要说明一个更好的方式来进行编程。把成员变量所有写进private中,来实现数据的封装。使用public的get set方法来得到和修改这个变量。

构造函数和析构函数

C++和Java不同,其中一个重要的区别就是:C++编译器强制要求程序员进行内存管理,而Java的内存管理是由JVM(Java虚拟机)代替程序员完成。因此,这里引出一个重要的概念(和C语言同样):内存管理对于C/C++开发者来讲是一个永恒的话题

构造函数

  • 构造函数必须写在public下。
  • 构造函数没有类型,也没有返回值(不要多此一举写void)
  • 构造函数的函数名必须和类名一致。
  • 构造函数能够重载。(请先无视掉重载,在多态的部分会详细介绍)
  • 构造函数是由编译器自动调用,并且只调用一次。
class Person
{
public:
    Person()
    {
        std::cout<<"我是构造函数"<<std::endl;
    }
};
构造函数的分类
  • 有参构造函数
  • 无参构造函数(默认构造函数)
class Person
{
public:
    Person()
    {
        cout<<"默认构造函数"<<endl;
    }    
    Person(int a)
    {
        age = a;
        cout<<"有参构造函数"<<endl;
    }
};

通常状况,编译器会提供默认的构造函数,若是程序员手动写了有参的构造函数,那么编译器便不会提供默认的构造函数。

void test()
{
    //构造函数调用方式
    //括号法调用
    Person p1(1);
    p1.age = 10;
    Person p2(p1);
     
    cout<<"p2年龄:"<<p2.Age<<endl;
    Person p3;//默认构造函数不加(); Person p3() 编译器任务这行是函数声明
    
    //显示法调用
    Person p4 = Person(100);//Person(100)叫匿名对象
    Person p5 = Person(p4);
    Person(100);//若是编译器发现了匿名对象,那么在这行代码结束后,就释放这个对象
}

咱们能够经过以上方法对构造函数进行调用。
注意:上述语句中出现匿名对象。顾名思义,匿名对象就是没有名字,这种对象在这行代码结束后,就释放这个对象。(匿名对象未来会有伏笔,如今请记住!)

【补充】初始化列表:这里是有参构造函数的一种写法:

class foo 
{ 
public: 
    foo(string s, int i):name(s), id(i){} ; // 初始化列表 
    //相应的含义:把s的值赋值给name,把i的值赋值给id
private: 
    string name ;
    int id ; 
};

使用初始化列表的缘由,主要是由于性能。对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差异不是很大,可是对于类类型来讲,最好使用初始化列表,使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来讲,是很是高效的。因此,咱们能使用初始化列表的时候尽量使用初始化列表。
固然,有的时候咱们也必须使用初始化列表:

  • 常量成员,由于常量只能初始化不能赋值,因此必须放在初始化列表里面
  • 引用类型,引用必须在定义的时候初始化,而且不能从新赋值,因此也要写在初始化列表里面
  • 没有默认构造函数的类类型,由于使用初始化列表能够没必要调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
有关拷贝构造函数
class Person
{
public:
     Person(const Person& p)
    {
        Age = p.Age;
        cout<<"拷贝构造函数"<<endl;
    }
};

所谓拷贝构造函数就是把构造函数复制了一份

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

  • 用已经建立好的的对象初始化新的对象
  • 用值传递的方式给函数参数传值
  • 用值的方式返回局部对象
class Person
{
public:
    Person()
    {
        cout<<"默认构造函数调用"<<endl;
    }
    Person(int a)
    {
        cout<<"有参构造函数调用"<<endl;
    }
    Person(const Person& p)
    {
        cout<<"拷贝构造函数调用"<<endl;
    }  
    ~Person()
    {
        cout<<"析构函数调用"<<endl;
    } 
    int Age;
};
//用已经建立好的的对象初始化新的对象
void test()
{
    Person p1 ;
    p1.Age = 10;
    
    Person(p1);
}
//用值传递的方式给函数参数传值
void doWork(Person p1)
{
    
}

void test02()
{
    Person p;
    p.Age;
    
    doWork(p);
}

//用值的方式返回局部对象
Person doWork2()
{
    Person p1;
    return p1;
}
void test03()
{
    Person p = doWork();
}

//请注意debug模式下和release模式下的区别
//Release模式下的优化:
/*(节省一份开销)
Person  p;//不调用默认构造
doWork2(p);
void doWork2(Person &p)
{
    Person p1;//调用默认构造
}

~析构函数

  • 析构函数必须写在public下
  • 析构函数不能有返回值
  • 析构函数不能有参数
  • 析构函数的函数名与类名一致前面有一个~
  • 析构函数由编译器自动调用,且只调用一次
class Person
{
public:
    ~Person()
    {
        std::cout<<"我是析构函数"<<std::endl;
    }
};

析构函数的做用其实是作垃圾回收的。具体的内容再后面的内存管理部分会详细说明。

总结

这里只想说明一下编译器会为程序员默认的提供:无参构造函数,析构函数,拷贝构造函数,重载了赋值运算符(为知识完整性不得不说的,后面会细讲)

this指针

this指针的概念

C++的数据和操做是分开存储的,而且每个非内联函数成员只会诞生出一份函数实例,即多人同类型的对象公用一块代码
这一块代码是如何区分是哪一个对象调用本身呢?
引入this指针,this指针是指向被调用的成员函数所属的对象
C++规定,this指针是隐含在对象成员函数内的一种指针。当对象被建立后,它的每个成员函数都含有一个系统自动生成的隐含指针this,用来保存这个对象的地址。虽然咱们 没有写上this指针,编译器在编译的时候会自动加上。this也称指向本对象的指针。this指针并非对象的一部分,也不会影响sizeof(对象)的结果。
this指针是C++实现封装的一种机制,他将对象和该对象调用的成员函数链接在一块儿。从外部看来,每个对象都拥有本身的成员函数。通常状况下,并不写this,而是让系统系进行默认配置。
this指针永远指向当前对象。this指针是一种隐含的指针,它隐含于每个非静态的成员函数中

this指针的使用

class Person
{
public:
    Person(int age)
    {
        this->age = age;//编译器无法区分这几个age
        //区分有两种办法:1.换名字  2.this指针
    }    
     //对比年龄
     void compareAge(Person &p)
     {
         if(this->age = p.age)//默认给加了this,不写也行
         {
             cout<< "年龄相等"<<endl;
         }
         else
         {
             cout<<"年龄不等"<<endl;
         }
     } 
     //年龄相加
     Person& PlusAge(Person &p)//在这里若是去掉&,它将从一个引用传递变成值传递
     {
         this->age += p.age;
         return *this;//指向本体
     }
    int age;  
}

上面的语句不难理解,简单来讲就是为了区分名字。可是最后Person& PlusAge(Person &p)可能不少人感受奇怪,这实际上是this指针的第二个用途——链式编程。

p1.PlusAge(p2).PlusAge(p2).PlusAge(p2);//链式编程
    //若是变成值传递,每一次调用都是一个临时的空间里进行操做。并不会链式相加。这取决于你的需求

这种写法对于有Java基础的人来讲必定不陌生。

补充const修饰成员函数

class Person
{
public:
    Person()
    {
        //构造中修改属性
        //this永远指向本体 至关于 Person * const this
        //虽然指针不能修改,可是能够修改指针指向的值。
        //若是我不但愿修改指针指向的值,则 const Person * const this
        this->A = 0;
        this->B = 0;
    }  
    
    //常函数
    void showInfo() const //不容许修改指针指向的值
    {    
        //this->C = 1000;
        // const Person * const this如何操做?,在函数()后面+const
        cout<<"A是"<<this->A<<endl;
        cout<<"B是"<<this->B<<endl;
        cout<<"C是"<<this->C<<endl;
    }  
    int A;
    int B;
    //若有即使是常函数也要修改的需求,可使用关键字mutable
    mutable int C;
}

固然,若是你真的理解了this指针的原理,则还能够以上这种C++风格的代码改写成C风格

//C++风格
void showInfo() const
{
...
}
//C风格
void showInfo(cosnt Person * this)
{
...
}

若是这样改写的话,在调用时也要改写

//C++风格
p.showInfo();
//C风格
p.showInfo(&p);

对象数组

有关对象数组,也是顾名思义,就是一个元素是对象的数组
Person xiaoxue[4];
这里只有须要注意的地方:要建立的对象数组,则这个类必须有默认构造函数
若是但愿使用构造函数来初始化对象数组,则:

Person p[2] = {
    Person(10),//10岁
    Person(12)//12岁
};
//也可使用不一样的构造函数
Person p[2] = {
    Person(10),//10岁
    Person()//使用默认构造函数
};

类的做用域

概念

咱们以前就知道全局做用域和局部做用域。在这里C++引入新的做用域——类做用域
类中定义的名称的做用域是整个类,做用域为整个类的名称只有在该类的做用域中是已知的,类外是不可知的。因此,不一样类使用相同的名字并不会冲突。
若是是类外的话,咱们可使用直接成员操做符(.),间接成员操做符(->),做用域操做符(::)来访问类中的public方法。前提是应该使用前包含类声明的头文件。

做用域是整个类的常量

或许会有人这样去书写代码:

class Person
{
private:
    const int Len = 30; 
    char Arr[Len];
};

这是不可行的,但确是一个常见的误区。
首先解释一下为何不可行:声明类只是描述类的形式,并无真正的建立对象。所以,在对象建立以前并无可使用的存储空间。
虽然上述的写法是错误的,可是咱们可使用别的方法达到咱们的目的。

class Person
{
private:
    enum{Len = 30};//使用枚举 
    char Arr[Len];
};

注意,这种方式声明枚举并不会建立数据成员。即全部对象并不包含枚举。并且Len仅仅是一个符号,当在做用域为整个类的代码中遇到它时,编译器将用30来替换。

至于第二种方法,咱们就要引入C++中一个很是重要的关键字 static。

static关键字
  • 静态成员变量

在一个类中,若将一个成员变量声明为static,这种成员变量称为静态成员变量。与通常的成员变量不一样,不管创建多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,对全部对象共享(其中一个修改了,其余的也都修改了)。
静态变量,在编译阶段就分配空间,对象尚未建立时,就已经分配空间

class Person
{
public:
    Person()
    {
        //Age = 10;//虽然不会报错,通常不会这么去作。
    }
    static int Age;// 在类内声明,在类外定义。会报错!
    
    //静态成员变量也是有权限的
private:
    static int other;//私有权限,类内类外不能访问    
};
int Person::Age = 0;
int Person::other = 0;//仍然算类内访问
  • 静态成员函数
class Person
{
public:
    Person()
    {
        //Age = 10;
    }
    static int Age;
    //静态成员函数
    //静态成员函数不能够访问普通的成员变量
    //能够访问静态成员变量
    static void func()
    {
        cout<<"func调用"<<endl;
    }
    //普通成员函数能够访问普通成员变量,也能够访问静态的成员变量
   void MyFunc()
   {
       
   }
private:
    static int other; 
    //静态成员函数也有权限
     static void func1()
    {
        cout<<"func1调用"<<endl;
    }
};
int Person::Age = 0;
int Person::other = 0;

在咱们了解了static关键字时,咱们就已经能够解决刚刚的问题了:

class Person
{
private:
    static const int Len = 30; 
    char Arr[Len];
};

这里将建立一个静态的Len常量,这个常量将于其余的静态成员一块儿存储,而不是存储在对象中。(之后还会有机会遇到static)

相关文章
相关标签/搜索