3. C++类的成员变量和成员函数

类的成员变量和普通的变量一样,从格式上基本没多区别。

唯一需要注意是他们的责任是不同的,成员变量是对对象负责的,在类中,使用范围由类决定,而普通变量则没有这个说法。

类的成员函数也和普通函数一样,都有返回值和形参。

它与普通函数的区别是:成员函数是一个类的成员,出现在类中,它的作用范围由类来决定;而普通函数是独立的,作用范围是全局的,或位于某个命名空间内。

这里不同变量和不同的函数我们后面会做一个系统的解释和分析。

下面还是之前的例程,成员函数类内声明,类外定义。

ps:早期版本的C++成员变量声明时不可以对其初始化,后期C++ 11,可以进行成员变量初始化赋值。

#include <iostream>
using namespace std;

//类通常定义在函数外面
class Person{
public:
    //类包含的变量
    char *name;
    int age;
    //类内声明
    void say();
};

//类外定义
void Person::say(){
    cout << name << "的年龄是" << age << endl;
}

int main(){
    //创建对象
    Person p;
    p.name = "豆豆";
    p.age = 16;
    p.say();
    return 0;
}

void Person::say()

::被称为作用域运算符或作用域限定符,用来连接类名和函数名,指明当前函数属于哪个类,成员函数在类外定义时必须使用作用域限定符。

注意:在引入了类的类型后,我们再参数和返回值会有更多的选择

#include <iostream>
using namespace std;

class Car{
public:
    string name;
    string color;
    int wheel;
public:
    void run();
};

void Car::run()
{
    cout << color << "的" << name << "在跑..." << endl;
}

class CarFatory{
public:
    string name;
    string address;
    string tel;
public:
    Car* repair(Car *c);
};

Car* CarFatory::repair(Car *c)
{
    if(c->wheel < 4){
        c->wheel = 4;
        cout << c->name << "车,修好了" << endl;
    }
    return c;
}

int main(){
    Car *c = new Car();
    c->name = "保时捷";
    c->color = "红色";
    c->run();//车在跑
    c->wheel = 3;//跑着跑着车轮子掉了了,坏了
    CarFatory *f = new CarFatory();
    Car *newCar = f->repair(c);//修车
    cout << "车有" << newCar->wheel << "个轮子" << endl;
    return 0;
}

在一个类中成员中变量和函数可以分为多种形态
我们先看下成员变量的部分
在C中我们经常遇到这几个混乱的变量

局部变量:在一个函数内部定义的变量(包括函数形参)是局部变量,存储在栈内存,在函数结束后自动销毁。

全局变量:在函数体外定义的变量,可以为本源文件中其它函数所公用,有效范围为从定义变量的位置开始到本源文件结束,这种类型的变量就称为“全局变量”。全局变量存储在静态存储区域(静态内存)。

ps:全局变量可以被同一工程项目中其他文件用extern声明后调用,对其每次进行修改都会被保存。

静态变量又分为:静态全局变量和静态局部变量

静态全局变量:在原先的全局变量前面加上了static进行修饰。存储在静态存储区。跟全局变量最大的不同在于,静态全局变量不能被其他源文件使用,只能被本源文件使用,对其每次进行修改都会被保存。

静态局部变量:在原先的局部变量前面加上了static进行修饰。存储在静态存储区内,等到整个程序结束才会被销毁,但是它的作用域依然在函数体内部。

ps:静态局部变量一般实际中没有太大作用,所以这里我们了解下就可以。


需要重点关注的几个部分:

1、静态成员变量

class Person{
public:
    void show();
public:
     static int height; //静态成员变量
private:
    char *name;
    int age;
};

静态成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为height分配一份内存,所有对象使用的都是这份内存中的数据。
当某个对象修改了height,也会影响到其他对象。

注意:
1、静态成员变量必须在类声明的外部初始化,而且只能在类体外进行。

int Person::height = 0;

初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。
全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。

2、静态成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在类外初始化时分配。
3、一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量。
4、静态成员变量和普通静态变量一样,都在内存分区中的全局数据区分配内存,程序结束时才释放。
5、静态成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
6、静态成员变量既可以通过对象名访问,也可以通过类名访问。

关于静态成员变量的访问方式:

//通过类类访问 static 成员变量
Person::height= 180;

//通过对象来访问 static 成员变量
Person p;
p.height= 20;

//通过对象指针来访问 static 成员变量
Person *p = new Student();
p->height= 190;

-----

2、静态成员函数

C++中成员函数也是可以声明为静态成员函数的,静态成员函数只能访问静态成员。

编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。

静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。

静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。

include <iostream>

using namespace std;

class Person{
public:
void show();
public: //声明静态成员函数
static int getAge();
static double getSalary();
private:
static int age;
static double salary;
private:
char *name;
};

int Person::age = 20;
double Person::salary = 5000.0;

void Person::show(){
cout<< name <<"的年龄是"<< age <<",工资是"<< salary <<endl;
}

//定义静态成员函数
int Person::getAge(){
return age;
}

double Person::getSalary(){
return salary;
}

int main(){
int age = Person::getAge();
float salary = Person::getSalary();
cout<<"年龄"<< age <<"的员工工资是"<< salary <<endl;
return 0;
}

-----

3、空类的默认成员函数

关于C++成员函数这是我们比较关注的

让我们看一下空类中都有什么样的成员函数,编译器会为空类提供哪些默认成员函数?分别有什么样的功能呢?

空类,声明时编译器不会生成任何成员函数,对于空类,编译器不会生成任何的成员函数,只会生成1个字节的占位符。(在Linux下,是4个字节)

C++空类编译器自动生成的6个成员函数:
一个缺省的构造函数
一个拷贝构造函数
一个析构函数
一个赋值运算符
两个取址运算符。

class Empty
{
public:
Empty(); //缺省构造函数
Empty(const Empty &rhs); //拷贝构造函数
~Empty(); //析构函数
Empty& operator=(const Empty &rhs); //赋值运算符
Empty* operator&(); //取址运算符
const Empty* operator&() const; //取址运算符(const版本)
};

使用时的调用情况:

Empty *e = new Empty(); //缺省构造函数
delete e; //析构函数
Empty e1; //缺省构造函数
Empty e2(e1); //拷贝构造函数
e2 = e1; //赋值运算符
Empty *pe1 = &e1; //取址运算符(非const)
const Empty *pe2 = &e2; //取址运算符(const)

C++编译器对这些函数的实现:

inline Empty::Empty() //缺省构造函数
{
}

inline Empty::~Empty() //析构函数
{
}

inline Empty *Empty::operator&() //取址运算符(非const)
{
return this;
}

inline const Empty *Empty::operator&() const //取址运算符(const)
{
return this;
}

inline Empty::Empty(const Empty &rhs) //拷贝构造函数
{
//对类的非静态数据成员进行以"成员为单位"逐一拷贝构造
//固定类型的对象拷贝构造是从源对象到目标对象的"逐位"拷贝
}

inline Empty& Empty::operator=(const Empty &rhs) //赋值运算符
{
//对类的非静态数据成员进行以"成员为单位"逐一赋值
//固定类型的对象赋值是从源对象到目标对象的"逐位"赋值。
}

m是类C中的一个类型为T的非静态成员变量,若C没有声明拷贝构造函数(赋值运算符), m将会通过T的拷贝构造函数(赋值运算符)被拷贝构造(赋值);该规则递归应用到m的数据成员,直到找到一个拷贝构造函数(赋值运算符)或固定类型(例如:int、double、指针等)为止。

这些函数我们后续会依次进行讲解

----

4、构造函数

构造函数(Constructor): 它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。

格式:
声明:

类名(参数列表);

类外定义:

类名 :: 类名(参数列表) : 构造函数的初始化列表{ 函数体 }

通过构造函数可以在创建对象的同时,对对象的成员变量(属性)进行初始化,这样就简化了创建对象后再赋值属性值的过程。

include <iostream>

using namespace std;

class Student{
private:
char *name;
int age;
float score;
public:
//声明构造函数
Student(char *name, int age, float score);
//声明普通成员函数
void show();
};

//定义构造函数
Student::Student(char *name, int age, float score){
this->name = name;
this->age = age;
this->score = score;
}

//定义普通成员函数
void Student::show(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}

int main(){
//创建对象时向构造函数传参
Student stu("豆豆", 20, 93.0);
stu.show();
//创建对象时向构造函数传参
Student *pstu = new Student("哈哈", 21, 96.0);
pstu->show();
return 0;
}

![image.png](https://upload-images.jianshu.io/upload_images/16823531-5062de1424bbdadd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表,从而使代码更加简洁。

include <iostream>

using namespace std;

class Student{
private:
char *name;
int age;
float score;
public:
Student(char *name, int age, float score);
void show();
};

//采用初始化列表
Student::Student(char *name, int age, float score): name(name), age(age), score(score){
//TODO:
}

void Student::show(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}

int main(){
Student stu("豆豆", 20, 93.0);
stu.show();
Student *pstu = new Student("哈哈", 21, 96.0);
pstu->show();
return 0;
}

注意:
1、构造函数必须是 public 属性的,否则创建对象时无法调用。
2、构造函数没有返回值。
3、函数体中不能有 return 语句。
4、使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便。
5、初始化列表可以用于全部成员变量,也可以只用于部分成员变量。
6、成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。

#include <iostream>
using namespace std;

class Demo{
private:
    int a;
    int b;
public:
    Demo(int b1);
    void show();
};

Demo::Demo(int b1): b(b1), a(b){ }
void Demo::show(){ cout<< a <<", "<< b <<endl; }

int main(){
    Demo obj(100);
    obj.show();
    return 0;
}
image.png

上面程序初始化列表等价于

Demo::Demo(int b1): m_b(b1), m_a(b){
    a = b;
    b = b1;
}

给 a 赋值时,b 还未被初始化,它的值是不确定的,所以输出的 a 的值是一个奇怪的数字;
obj 在栈上分配内存,成员变量的初始值是不确定的。
好像感觉初始化列表除了简洁没有其他作用,实则不然,初始化 const 成员变量的唯一方法就是使用初始化列表。

class Array{
private:
    const int len;
    int *arr;
public:
    Array(int len);
};

//必须使用初始化列表来初始化 len
Array::Array(int len): len(len){
    arr = new int[len];
}

默认构造函数

如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。

Student(){}

一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。
一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。
注意:最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。
在栈上创建对象可以写作Student stu()或Student stu
在堆上创建对象可以写作Student *pstu = new Student()或Student *pstu = new Student
它们一样都会调用构造函数 Student()。


5、构造函数的重载

说到构造函数重载,我们就需要说一下重载的概念了。

函数重载是一种特殊情况,C++允许在同一作用域中声明几个类似的同名函数,这些同名函数的形参列表(参数个数,类型,顺序)必须不同,

常用来处理实现功能类似数据类型不同的问题。

//全局的函数重载
int get();
int get(int a);
int get(float a);
int get(int a, int b);

class Calculate{
private:
    int a;
    int b;
public:
    //构造函数重载
    Calculate();
    Calculate(int a);
    Calculate(int a, int b);

    //成员函数重载
    void sum();
    //int sum();   //不是重载,与返回值无关
    void sum(int a, int b);
    void sum(int a, int b, int c);
    void sum(double a, double b);
};

void Calculate::sum(){}
//int Calculate::sum(){}
void Calculate::sum(int a, int b){}
void Calculate::sum(int a, int b, int c){}
void Calculate::sum(double a, double b){}

后续我们还会继续讨论重载。


6、析构函数

析构函数也是一种特殊的成员函数,没有返回值,不需要程序员显式调用,而是在销毁对象时自动执行。
构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

注意:
1、析构函数没有参数,不能被重载
2、一个类只能有一个析构函数。
3、如果用户没有定义析构函数,编译器会自动生成一个默认的析构函数。

#include <iostream>
using namespace std;

/*
封装一个数组类来看delete的作用
*/
class Array{
public:
    Array(int len); //构造函数
    ~Array(); //析构函数
public:
    void input(); //输入数组元素函数
    void out(); //显示数组元素函数
private:
    int* getElement(int i); //获取第i个元素的指针
private:
    const int len; //数组的长度
    int *arr; //数组指针
    int *p; //指向数组元素的指针
};

Array::Array(int len): len(len){ //使用初始化列表来给len赋值
    if(len > 0){
        arr = new int[len]; //动态内存申请一个块用于数组的内存
    }
    else{
        arr = NULL;
    }
}

Array::~Array(){
    delete[] arr; //释放内存
}
void Array::input(){
    for(int i = 0; p = getElement(i); i++){
        cin>>*getElement(i);
    }
}

void Array::out(){
    for(int i = 0; p = getElement(i); i++){
        if(i == len - 1){
            cout<<*getElement(i)<<endl;
        }
        else{
            cout<<*getElement(i)<<", ";
        }
    }
}

int * Array::getElement(int i){
    if(!arr || i < 0 || i >= len){
        return NULL;
    }
    else{
        return arr + i;
    }
}

int main(){
    int n;
    cout<<"输入数组的长度: ";
    cin>>n;
    Array *parr = new Array(n); //创建一个有n个元素的数组对象
    //输入数组元素
    cout<<"请输入 "<<n<<" 个元素: ";
    parr->input();
    //输出数组元素
    cout<<"数组内元素是: ";
    parr->out();
    //删除数组(对象)
    delete parr;
    return 0;
}
image.png

注意:
1、new 分配内存时会调用构造函数。
2、delete 释放内存时会调用析构函数。
3、构造函数和析构函数对于类来说是不可或缺的。

析构函数的调用时机
析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。

#include <iostream>
#include <string>
using namespace std;

class Test{
public:
    Test(string s);
    ~Test();
private:
    string s;
};

Test::Test(string s): s(s){ cout<<this->s<<"构造函数调用"<<endl; }
Test::~Test(){ cout<<s<<"析构函数调用"<<endl; }

void function(){
    //局部对象
    Test obj1("对象1");
}

//全局对象
Test obj2("对象2");

int main(){
    function();
    //局部对象
    Test obj3("对象3");
    //new创建的对象
    Test *pobj4 = new Test("对象4");
    return 0;
}
image.png

7、拷贝构造函数(复制构造函数)

拷贝构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。
当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数。
当该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用复制构造函数。

C++支持两种初始化形式:

复制初始化   int a = 5;
直接初始化   int a(5);

对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,复制初始化总是调用复制构造函数,也就是说:

A x(2);  //直接初始化,调用构造函数
A y = x;  //复制初始化,调用复制构造函数

必须定义复制构造函数的情况:
只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数也可以复制;
有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义复制构造函数。

什么情况使用复制构造函数:
类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体
(2)一个对象以值传递的方式从函数返回
(3)一个对象需要通过另外一个对象进行初始化。

深拷贝和浅拷贝:
浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间
深拷贝:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是重新动态分配空间

如果没有自定义拷贝构造函数,则系统会创建默认的拷贝构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象;

若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错。

下面是示例:

#include <iostream>
#include <string.h>
using namespace std;

class Person
{
public :
    // 构造函数
    Person(char * pN);
    // 系统创建的默认复制构造函数,只做位模式拷贝
    Person(Person & p);
    ~Person();
private :
    char* pName;
};

Person::Person(char * pN){
    cout << "构造函数被调用"<<endl;
    pName = new char[strlen(pN) + 1];
    //在堆中开辟一个内存块存放pN所指的字符串
    if(pName != NULL)
    {
        //如果pName不是空指针,则把形参指针pN所指的字符串复制给它
        strcpy(pName ,pN);
    }
}

Person::Person(Person &p){
    //使两个字符串指针指向同一地址位置
    pName = p.pName;
}

Person::~Person(){
    delete pName;
    cout << "析构函数被调用"<<endl;
}

int main( )
{
    /*p1和p2的指针都指向了同一个地址
    函数结束析构时
    同一个地址被delete两次
    */

    Person p1("豆豆");
    Person p2(p1);

    return 0;
}
image.png
// 下面自己设计复制构造函数,实现“深拷贝”,即不让指针指向同一地址,而是重新申请一块内存给新的对象的指针数据成员
Person::Person(Person & p)
{
    // 用运算符new为新对象的指针数据成员分配空间
    pName = new char[strlen(p.pName)+ 1];
    if(pName)
    {
    // 复制内容
    strcpy(pName ,p.pName);
    }
    // 则新创建的对象的pName与原对象chs的pName不再指向同一地址了
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容