C++入门教程 04

学习链接

四、类和对象

1. 类的定义

通俗的理解,类其实就是一个模子,是一个变量类型,对象就是这个类型定义出来的具体的变量,就像int a;这句话,int对应类,a就对应对象。

概括的讲:类是对象的抽象和概括,而对象是类的具体和实例。

那么C++中的类长什么样呢?简单说, C++中类的其实就是包含函数的结构体!因为C++类里面的成员除了可以像C语言的结构体那样包含基本变量以外,还可以包含函数,前者叫做成员变量,后者叫做成员方法。

关键字用 class/struct 类定义,比如下面定义一个C++的类,学生类:

class Student
{
public:
    int num;
    char name[100];
    int score;
    int print()
    {
        cout<<num<<" "<<name<<" "<<score;
        return 0;
    }
};	// 注意有分号

形式上和C语言的结构体非常像,成员有变量也有函数,今后要习惯称之为属性和方法了。

类里还有一个public的东西,它是控制成员访问权限的一个存取控制属性,除了public以外,还有private、protected一共三种,

  • 其中private表示私有,被它声明的成员,仅仅能被该类里的成员访问,外界不能访问,是最封闭的一种权限;
  • protected比private稍微公开一些,除了类内自己的成员可以访问外,它的子类也可以访问
  • 而public声明的成员,则可以被该类的任何对象访问,是完全公开的数据。

如果类里的成员函数很多的话,阅读起来就会乱很多,故此,C++还支持另外一种写法,就是成员函数仅在类内声明函数原型,在类外定义函数体,这样在类里可以看到所有成员函数的列表,像目录一样一目了然,规范很多。

class Student
{
public:
    int num;//学号
    char name[100];//名字
    int score;//成绩
    int print();//类内声明print函数
};
int Student::print()//在类外定义完整的print函数
{
    cout<<num<<" "<<name<<" "<<score;
    return 0;
}

2. 对象的建立和使用

1、对象的创建

类就是包含函数的结构体,是一种自定义数据类型,用它定义出来变量,就是对象,这就是所谓的“对象是类的具体和实例”,定义了一个这个类的对象,也可以说实例化了一个对象,就是这个意思!

对象的使用,和结构体的使用也一样,都是主要访问里面的成员,也都是用过.的方式来访问,如:

Student A;
A.num = 101;
strcpy(A.name,"dotcpp");
A.score = 100;
A.print();

需要注意的是,这里类中的成员变量都是声明为public类型的,如果声明为private类型,则在主函数中主要通过对象.变量的方式直接访问的话就会被禁止报错,原因private类型的变量是私有类型,不允许外部访问。

对于想保护但又想控制的私有变量,我们通常将其声明为private类型,然后同时定义一个public类型的专门赋值的方法,由于内部成员可以访问private声明的变量,我们就可以在外部通过这个public的方法来间接控制这些私有的成员,来起到封装、保护的效果,而这个public类型的方法,也称之为这个类的一个外部接口!

2、对象的指针

与普通变量一样,对象也是一片连续的内存空间,因此也可以创建一个指向对象的指针,即对象指针,存储这个对象的地址。

定义方法如下:

类名 *指针名;
如定义Student *p;

定义一个Clock类型的指针p,需要清楚的是,这里并没有建立对象,当然也不会调用构造函数。
接下来就可以将一个同类型的类对象地址赋值给这个指针,然后通过->来访问对象中的成员。

Student *p;
Student A;
p = &A;
p->print();

除了在赋值、访问成员的时候用以外,在传参的时候也建议用指针来传递,因为其传递的为地址,不会进行对象之间的副本赋值,从而减少内存的开销,提高效率。

3、对象的引用

引用,是C++中一种新的类型,对象引用就是一个类对象起个别名,本质上也是把这个类对象的地址赋给了这个引用类型,两者是指向一块内存空间的。

Student A;
Student &Aq=A;

如以上代码,定义一个Student类型的对象,然后用&来定义一个该类类型的引用类型,并把A对象赋给Aq作为初始化。

注意!

  1. 与指针一样,两者必须是同类型才可以引用。
  2. 除非做函数的返回值或形参时,其余定义引用类型的同时就要初始化!
  3. 引用类型并非是新建立一个对象,因此也不会调用构造函数。

既然是类对象的别名,因此使用方法也和类对象一样,用别名.成员的方法进行访问,如:

Student A;
Student &Aq=A;
Aq.print();

引用类型的优点:
可以看到,用引用类型时,本质还是存的地址,因此无论传参定义都不会太多内存开销,有指针的优势,同时使用起来和类对象本身使用一样,在做函数实参时,直接传入引用对象就可以,不用加地址符,因此看起来更直观、方便。

3. 构造函数constructor

一种特殊的函数,它在类里,与类名同名,且没有返回值。只要我们定义一个类的对象,系统就会自动调用它,进行专门的初始化对象用,而大多数情况下,因为我们没有定义构造函数,系统会默认生成一个默认形式、隐藏着的构造函数,这个构造函数的函数体是空着的,因此不具有任何功能。

如何定义自己的构造函数?用户自行定义了至少一个构造函数,系统就不在自动生成,而是根据用户定义的构造函数选择最匹配的一个进行调用。

#include<iostream>
#include<cstring>

using namespace std;

class Student {
public:
    Student(int n,char *str,int s);
    int print();
    int Set(int n,char *str,int s);

private:
    int num;//学号
    char name[100];//名字
    int score;//成绩
};
Student::Student(int n,char *str,int s) {
    num = n;
    strcpy(name,str);
    score = s;
    cout<<"Constructor"<<endl;
}
int Student::print() {
    cout<<num<<" "<<name<<" "<<score;
    return 0;
}
int Student::Set(int n,char *str,int s) {
     num = n;
     strcpy(name,str);
     score = s;
}
int main() {
    Student A(100,"dotcpp",11);
    A.print();
    return 0;
}

4. 析构函数destructor

类对象在创建时自动调用的构造函数,在对象销毁时也会自动调用一个函数,它也和类名同名,也没有返回值,名字前有一个个波浪线~,用来区分构造函数,它的作用主要是用做对象释放后的清理善后工作。它,就是析构函数。

与构造函数相同的是,与类名相同,没有返回值,如果用户不定义,系统也会自动生成一个空的析构函数。而一旦用户定义,则对象在销毁时自动调用。

与构造函数不同的是,虽然他俩都为公开类型。构造可以重载,有多个兄弟,而析构却不能重载,但它可以是虚函数,一个类只能有一个析构函数。

#include<iostream>
#include<cstring>

using namespace std;

class Student {
public:
    Student(int n,char *str,int s);
    ~Student();
    int print();
    int Set(int n,char *str,int s);

private:
    int num;//学号
    char name[100];//名字
    int score;//成绩
};
Student::Student(int n,char *str,int s) {
    num = n;
    strcpy(name,str);
    score = s;
    cout << num << " " << name << " " << score << " ";
    cout<<"Constructor"<<endl;
}
Student::~Student() {
    cout << num << " " << name << " " << score << " ";
    cout << "destructor" << endl;
}
int Student::print() {
    cout<<num<<" "<<name<<" "<<score;
    return 0;
}
int Student::Set(int n,char *str,int s) {
     num = n;
     strcpy(name,str);
     score = s;
}
int main() {
    Student A(100,"dotcpp",11);
    Student B(101, "cpp", 12);
    return 0;
}

image-20211022160830959

可以看到对象A和B的构造函数的调用顺序以及构造函数的调用顺序,完全相反!原因在于A和B对象同属局部对象,也在栈区存储,也遵循“先进后出”的顺序!

5. 拷贝构造函数

在C++中,与类名同名,且形参是本类对象的引用类型的函数,叫做拷贝构造函数(Copy Constrctor),与构造函数一样,当我们不主动定义的时候,系统也会自动生成一个,进行两个对象成员之间对应的简单赋值,用来初始化一个对象。

示例:

#include<iostream>
using namespace std;
#define PI 3.1415
class Circle
{
private:
    double R;
public:
    Circle(double R);
    Circle(Circle &A);
    double area();
    double girth();
};
Circle::Circle(double R)
{
    cout<<"Constructor"<<endl;
    this->R = R;
}
Circle::Circle(Circle &A)
{
    cout<<"Copy Constructor"<<endl;
    this->R = A.R;
}
double Circle::area()
{
    return PI*R*R;
}
double Circle::girth()
{
    return 2*PI*R;
}
int main()
{
  
    Circle A(5);
    Circle B(A);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkcbHZ1h-1634891964069)(https://gitee.com/ClimberCoding/picturebed/raw/master/img/image-20211022162129922.png)]

一个额外的示例:

#include <iostream>

using namespace std;

class Line {
public:
    int getLength(void);
    Line(int len);      // 简单的构造函数
    Line(const Line& obj);  // 拷贝构造函数
    ~Line();            // 析构函数
private:
    int* ptr;
};

// 成员函数定义
Line::Line(int len) {
    cout << "调用构造函数" << endl;
    // 为指针分配内存
    ptr = new int;
    *ptr = len;
}

Line::Line(const Line& obj) {
    cout << "调用拷贝构造函数,并为指针ptr分配内存" << endl;
    ptr = new int;
    *ptr = *obj.ptr;    // 拷贝值
}

Line::~Line(void) {
    cout << "释放内存" << endl;
    delete ptr;
}

int Line::getLength(void) {
    return *ptr;
}

void display(Line obj) {
    cout << "line大小:" << obj.getLength() << endl;
}

int main() {
    Line line{10};

    Line line2 = line;  // 这里也用了拷贝构造函数

    display(line);
    display(line2);

    return 0;
} 

image-20211022161239837

默认的拷贝构造函数仅仅是做简单的赋值,有些情况则要出现问题,这就涉及到深拷贝与浅拷贝

6. 浅拷贝与深拷贝

刚才的Circle类中,如果成员变量中加一个指针成员,初始化中需要动态开辟内存,则会出现极大的安全隐患,代码如下:

#include <iostream>
#include <cstring>
using namespace std;
#define PI 3.1415
class Circle
{
private:
    double R;
    char* str;
public:
    Circle(double R, char* str);
    //Circle(Circle &A);
    ~Circle();
    double area();
    double girth();
};
Circle::Circle(double R, char* str)
{
    cout<<"Constructor"<<endl;
    this->R = R;
    this->str = new char[strlen(str) + 1];
    strcpy(this->str, str);
    cout << this->R << " " << this->str << endl;
}
// Circle::Circle(Circle &A)
// {
//     cout<<"Copy Constructor"<<endl;
//     this->R = A.R;
// }
Circle::~Circle() {
    delete[] str;
}
double Circle::area()
{
    return PI*R*R;
}
double Circle::girth()
{
    return 2*PI*R;
}
int main()
{
  
    Circle A(5, "NO.1 Old class");
    Circle B(A);
    return 0;
}

image-20211022162658667

可以看出报错了,原因在于,默认的拷贝构造函数仅仅是进行数据赋值,并不能为指针开辟内存空间,相当于代码:This->str = str;

那么本质上,也就是两个指针指向一块堆空间。已经违背了我们的初衷。那么在程序结束的时候,两个对象回收的时候,会调用自己的析构函数,释放这块内存空间,由于两个对象要调用两次,即delete两次,就会出现错误!

所以,当类中有指针类型时,依靠默认的拷贝构造函数的方法,已经无法满足我们的需求,必须定义一个特定的拷贝构造函数,即不仅可以进行数据的拷贝,也可以为成员分配内存空间,实现真正的拷贝,也叫做深拷贝,这就是深拷贝构造函数。

#include <iostream>
#include <cstring>
using namespace std;
#define PI 3.1415
class Circle
{
private:
    double R;
    char* str;
public:
    Circle(double R, char* str);
    Circle(Circle &A);
    ~Circle();
    double area();
    double girth();
};
Circle::Circle(double R, char* str)
{
    cout<<"Constructor"<<endl;
    this->R = R;
    this->str = new char[strlen(str) + 1];
    strcpy(this->str, str);
}
Circle::Circle(Circle &A)
{
    cout<<"Copy Constructor"<<endl;
    this->R = A.R;
    this->str = new char[strlen(A.str) + 1];
    strcpy(this->str, A.str);
}
Circle::~Circle() {
    delete[] str;
    cout << "call destructor" << endl;
}
double Circle::area()
{
    return PI*R*R;
}
double Circle::girth()
{
    return 2*PI*R;
}
int main()
{
  
    Circle A(5, "NO.1 Old class");
    Circle B(A);
    return 0;
}

image-20211022163046972

其实现原理与带参数的构造函数类似,在赋值之前开辟足够的内存空间,来真正完成完整的拷贝,这就是所谓的“深拷贝”。

7. this指针

一个类当中,有一个很隐蔽的特殊指针,它就是—this指针!

为什么说它特殊?因为只要定义一个类,系统就会预定义个名字叫做this名且指向当前对象的指针。虽然我们看不到但却可以使用它。

如,来看一个时钟类的一个成员函数,用来设置时间传值的代码

int Clock::SetTime(int h,int m,int s) {
   H = h;
   M = m;
   S = s;
}

如果我们知道了this的存在,就可以这样写:

int Clock::SetTime(int h,int m,int s)
{
   this->H = h;
   this->M = m;
   this->S = s;
}
//也可以写成:
int Clock::SetTime(int h,int m,int s)
{
   (*this).H = h;
   (*this).M= m;
   (*this).S= s;
}

可以看到,以上两种写法用到了对象中的隐藏的this指针,可以明确是本类中的成员,从而明显的区别本对象与外部变量。

实际上,当一个对象调用其成员函数的时候,即便程序中有多个该类的对象,但成员函数的代码也仅有一份,所以为了区分它们是哪个对象调用的成员函数,编译器也是转化成this->成员函数这种形式来使用的。

8. 友元函数

我们都知道类中的私有成员,只有被类里的成员函数访问,在类外是不能访问的。这体现了C++中类设计的封装、隐蔽思想。是C++最基本的优点。

但如果偶尔有的时候,我们在类外又确实想访问这些私有成员,就会变得麻烦很多,就处于既访问不到又不能声明为public类型的两难处境。

而友元的出现就可以很好的解决这个问题。即把外部的函数声明为友元类型,赋予它可以访问类内私有成员的权利。来做到两全其美。这就是友元的意义,从字面意思也可以看出来,像“朋友”一样,开了一个绿色通道。

友元的对象,它可以是全局的一般函数,也可以是其他类里的成员函数,这种叫做友元函数。不仅如此,友元还可以是一个类,这种叫做友元类。

来看怎么在C++中使用friend

对于友元函数,只需要在类内对这个函数进行声明,并在之前加上friend关键字。这个函数就具有了独特的权限,成为友元函数。

注意,友元并不属于这个类本身,无论是友元函数还是友元类。都不能使用类内的this指针,同时也不可以被继承,如同父亲的朋友不一定是儿子的朋友这个道理。

#include<iostream>
#include<math.h>
using namespace std;
class Point
{
    private:
        double x;
        double y;
    public:
        Point(double a,double b)
        {
            x = a;
            y = b;
        }
        int GetPoint()
        {
            cout<<"("<<x<<","<<y<<")";
            return 0;
        }
        friend double Distance(Point &a,Point &b);
};
//求两点之间的距离
double Distance(Point &a,Point &b)
{
    double xx;
    double yy;
    xx = a.x-b.x;
    yy = a.y-b.y;
  
    return sqrt(xx*xx+yy*yy);
}
int main()
{
    Point A(2.0,3.0);
    Point B(1.0,2.0);
    double dis;
    dis = Distance(A,B);
    cout<<dis<<endl;
    return 0;
}

可以看到实现求两点之间距离的函数为外部的一般函数,由于需要访问类内的私有成员,所以我们把它在类内声明成frined友元类型。

注意,作为friend的函数Distance一定要在类Point之后定义。

9. 友元类

如果把一个类A声明为另一个类B的友元类,则类A中的所有成员函数都可以访问B类中的成员。使用方法也一样,在类B中进行声明即可。

#include<iostream>
#include<math.h>
using namespace std;
class Point
{
    private:
        double x;
        double y;
    public:
        Point(double a,double b)
        {
            x = a;
            y = b;
        }
        int GetPoint()
        {
            cout<<"("<<x<<","<<y<<")";
            return 0;
        }
        int distancetoLine() 
        {

        }
        friend class Tool;
};

class Tool
{
public:
    double GetX(Point &A)
    {
        cout<<A.x<<endl;
        return A.x;
    }
    double GetY(Point &A)
    {
        cout<<A.y<<endl;
        return A.y;
    }
    double dis(Point &A)
    {
        cout<<sqrt(A.x*A.x+A.y*A.y)<<endl;
        return  sqrt(A.x*A.x+A.y*A.y);
    }
};

int main()
{
    Point A(2.0,3.0);
    Tool T;
    T.GetX(A);
    T.GetX(A);
    T.dis(A);

    return 0;
}

一定要注意,上面作为friend的类A,一定要在类B之后定义。

最后我们总结友元机制的优缺点总结如下:

优点:更方便快捷的访问类内的私有成员
缺点:打破了C++中的封装思想

10. C++中常数据的使用及初始化

常的概念在学C语言时候就有了解,关键字是const,所谓的“常”,或或者说被“常”修饰的变量,是不可修改、被改变的,比如用const修饰的一个变量就成了常变量,这个值不可被更改。

那么C++中,一样有常的概念,额外不同的是,const除了可以修饰一般的变量为常变量之外,还可用于修饰某个对象,变成常对象。以及可以修饰类的数据成员和成员函数,分别叫做类的常数据成员常成员函数

1、常数据成员

格式:

数据类型 const  数据成员名;
const  数据类型 数据成员名;

被const修饰的成员则必须进行初始化,并且不能被更改。 而初始化的方式则是在类的构造函数的初始化列表里进行的。

特殊情况,如果成员是static类型,即静态常数据成员,因为是静态的属性,初始化则需在类外进行初始化,即便该数据成员为私有类型!

#include<iostream>
using namespace std;
class Clock
{
private:
    const int h;  //修饰h为常类型成员
    const int m;  //修饰m为常类型成员
    int const s;   //和上面两种用法都可以
    static const int x;
public:
    Clock(int a,int b,int c):h(a),m(b),s(c)
    {
        cout<<"Constrctor! Called"<<endl;
    }
    int ShowTime()
    {
        cout<<h<<":"<<m<<":"<<s<<endl;
        return 0;
    }
    int GetX()
    {
        cout<<x<<endl;
        return 0;
    }
};
const int Clock::x = 99;	// 类外初始化
int main()
{
    Clock A(12,10,30);
    A.ShowTime();
    A.GetX();
    return 0;
}
2、常对象

C++中可以把一个对象声明为const类型,即常对象。这样声明之后,这个对象在整个生命周期中就不可以再被更改,所以在定义的时候要由构造函数进行初始化。

格式:

类型 const 对象名;
const 类型 对象名;

注意,常对象不可以访问类中的非常成员函数,只能访问常成员函数。

成员变量不管是不是常量,常对象都可以访问,但是不能修改。

#include<iostream>
using namespace std;
class Clock
{
private:
    const int h;  //修饰h为常类型成员
    const int m;  //修饰m为常类型成员
    int const s;   //和上面两种用法都可以
    int x;
public:
    Clock(int a,int b,int c):h(a),m(b),s(c)
    {
        x=99;
        cout<<"Constrctor! Called"<<endl;
    }
    int ShowTime()
    {
        cout<<h<<":"<<m<<":"<<s<<endl;
        return 0;
    }
    int GetX() const
    {
        //x=99;
        cout<<x<<endl;
        return 0;
    }
    int a = 12;
};
  
int main()
{
    const Clock A(12,10,30);
    const Clock B(14,20,50);
    //A = B;	// A为常对象不可以被赋值
    //A.ShowTime();		// 常对象无法访问非常成员函数
    A.GetX();
    A.a;	// 常对象可以访问非常成员变量
    return 0;
}

注意看上面注释的两行,正常编译将报错

3、常成员函数

一个类中的成员函数被const修饰后,就变成了常成员函数,常成员函数的定义如下:

返回类型 函数名(形参表列)const;

注意:

  1. 常成员函数的定义和声明部分都要包含const;
  2. 常成员函数只能调用常成员函数,而不能调用非常成员函数,访问但不可以更改非常成员变量。
#include<iostream>
using namespace std;
class Clock
{
private:
    const int h;  //修饰h为常类型成员
    const int m;  //修饰m为常类型成员
    int const s;   //和上面两种用法都可以
    int x;
public:
    Clock(int a,int b,int c):h(a),m(b),s(c)
    {
        x=99;
        cout<<"Constrctor! Called"<<endl;
    }
    int ShowTime()
    {
        cout<<h<<":"<<m<<":"<<s<<endl;
        return 0;
    }
    int GetX() const
    {
        //x=99;
        cout<<x<<endl;	// 注意的第二条
        return 0;
    }
};
  
int main()
{
    const Clock A(12,10,30);
    A.GetX();
    return 0;
}

C++中常概念的建立,明确了程序中各对象的变与不变的边界,也加强了C++程序的安全性和可控性。


总结一下:

常数据成员:若有特殊的static常数据成员,就要在类外初始化,其余的都在构造函数列表中初始化。

常对象:只能访问常成员函数(函数后面加const的);数据成员不管是不是常量都能访问,但是不能修改。

常成员函数:在本函数中只能调用常成员函数,数据成员不管是不是常量都能访问,但是不能修改。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
"C入门教程电子书"是一本适用于初学者的编程教材。它涵盖了C语言的基础知识和主要概念,并通过简单易懂的方式解释和演示了各种编程技术和语法结构。这本电子书帮助读者了解C语言的发展历程和重要特点,同时引导他们逐步学习如何编写有效的C代码。 这本教程的主要特点之一是它的结构清晰且易于理解。它按照递进的顺序组织内容,从基本的变量和数据类型开始,然后介绍流程控制结构、函数的定义和使用以及指针的基本概念。读者可以通过逐个章节的学习,逐渐掌握C语言的核心概念和编程技巧。 此外,这本教程还提供大量的示例代码和练习,帮助读者巩固所学知识并培养自己的编程能力。这些示例代码涵盖了常见的编程任务和问题,并提供了解决方案和解释。通过实践,读者能够更好地理解和掌握C语言的实际应用。 另外,这本电子书还包含了一些编程技巧和最佳实践的建议,旨在帮助读者编写更干净、高效和可靠的代码。这些建议涵盖了代码注释、变量命名规范、错误处理和调试等方面。它们能够帮助读者养成良好的编程习惯,提高代码的质量和可读性。 总而言之,"C入门教程电子书"是一本全面而易懂的编程教材,适用于初学者。通过系统地学习和实践,读者能够掌握C语言的基础知识和编程技巧,并在实际项目中应用所学知识。这本教程为读者进一步深入学习C语言奠定了坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值