类的概念
从概念上来说:类是现实中具有共同特点食物的抽象。
这里举一个简单的例子:我们现在需要定义的一个人的类。所以我们要找出人和人的共同点(或者说人这个物种的共性),人有身高,体重,年龄,性别...这些就是人这个类的属性(成员变量)。人还会吃饭,走路,跳跃...这些就是人的方法(成员函数)。这样就可以抽象出人这个类。
类的定义和实现
当我们抽象出人这个类之后我们具体要怎么实现这个类呢?
请看如下语句
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这个对象就是具体到谁。
就像结构体那样:
xiaoming.run();
xiaoming.eat();
在这里所创建的每一个对象都有一个自己的存储空间,用于存储自己的成员变量和成员函数。但是多有对象都共享同一个类。
类成员的访问权限
这里先简单的了解一下,之后会在继承中详细指出。(友元可以暂时无视)
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
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)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。