c语言的struct只能包含变量,而c++中的class除了包含变量,还可以包含函数。
通过结构体定义出来的变量还是变量,而通过类定义出来有了新的名称,叫做对象。
C语言中,会将重复使用或具有某项功能的代码封装成一个函数,将拥有相关功能的多个函数放在一个源文件,再提供一个对应的头文件,这就是模块。使用模块时,引入对应的头文件就可。
而c++中,多了一层封装,就是类。类由一组相关联的函数,变量组成,你可以将一个类或多个类放在源文件,使用时引入对应的类就可以。如下图:
不要小看类(Class)这一层封装,它有很多特性,极大地方便了中大型程序的开发,它让 C++ 成为面向对象的语言。
c和c++中全局const变量的作用域相同,都是当前文件,不同的是他们的可见范围:
c语言中const全局变量的可见范围是整个程序,在其他文件中使用extern声明后
就可以使用;而c++中const全局变量的可见范围仅限于当前文件,在其他文件中不
可见,所以它可以在头文件中,多次引入后也不会出错。
int *p = new int; //分配1个int型的内存空间
delete p; //释放内存
int *p = new int[10]; //分配10个int型的内存空间
delete[] p;
内联函数:在函数调用处直接嵌入函数体的函数。可以提高效率,即在编译时将函数调用处用函数体替换,类似于c语言的宏展开。
指定内联函数:函数定义处增加inline关键字。如下例:
#include <iostream>
using namespace std;
//内联函数,交换两个数的值
inline void swap(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main(){
int m, n;
cin>>m>>n;
cout<<m<<", "<<n<<endl;
swap(&m, &n);
cout<<m<<", "<<n<<endl;
return 0;
}
结果:
45 99↙
45, 99
99, 45
使用内联函数的缺点也是非常明显的,编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大,所以再次强调,一般只将那些短小的、频繁调用的函数声明为内联函数。
在实际开发时,需要实现几个功能类似,但细节不同。如:交换两个变量的值,这两个变量有多种类型,可以是int,float,char,bool等。
对于每个不同类型都写一个函数,完成没有必要。
c++中允许多个函数拥有相同的名字,只要它们的参数列表(参数类型,参数个数和参数顺序)不同就可以,称为函数的重载。
创建对象:
Student liLei; //创建对象
Student allStu[100]; //创建对象数组
使用对象指针
1.在栈上分配内存:
Student stu;
Student *pStu=&stu;
2.在堆上创建对象
Student *pStu=new Student;
使用new在堆上创建出来的对象是匿名的,没法直接使用,必要时用一个指针指向它,再借助指针来访问它的成员变量或成员函数。
重点讲解了两种创建对象的方式:一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上使用 new 关键字创建,必须要用一个指针指向它,读者要记得 delete 掉不再使用的对象。
类是创建对象的模板,不占用内存空间,而对象是实实在在的数据,需要内存来存储。对象被创建
就会在栈区或者堆区分配内存。
编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但所有对象都共享同一段函数代码。
构造函数:
对成员变量进行初始化,在构造函数的函数体在对成员变量一一赋值,才可采用初始化列表、
1.成员变量的初始化与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
2.初始化const成员变量的唯一方法就是使用初始化列表。
this指针,是一个const指针,它指向当前对象,通过它可以访问当前对象的所有成员。
void Student::setname(char *name){
this->name = name;
}
this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this。
静态成员变量:static修饰。
实现:多个对象共享数据的目标。
1.static成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为其分配一份内存。当
某个对象修改了该值,也会影响其他对象,
2.static成员变量必须在类声明的外部初始化。
public:
static int m_total; //静态成员变量
int Student::m_total = 0;
3.static成员变量的内存既不是声明类时分配,也不在创建对象时分配,而是在类外初始化时分配。
4.static成员变量既可通过对象来访问,也可通过类来访问。
//通过类类访问 static 成员变量
Student::m_total = 10;
//通过对象来访问 static 成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通过对象指针来访问 static 成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
注意:static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
静态成员函数:
与普通成员函数区别:普通成员函数有this指针,可以访问类中的任意成员;而静态成员函数没有this指针,
只能调用静态成员函数。
const成员函数:
可以使用类中的所有成员变量,但不能修改它们的值,主要是为了保护数据而设置。也称常成员函数。
1.需要在声明和定义的时候在函数头部的结尾加上const关键字。
//声明常成员函数
char *getname() const;
//定义常成员函数
char * Student::getname() const{
return m_name;
}
区分一下cosnt的位置:
1.在函数开头的cosnt用来修饰函数的返回值,表示返回值是const类型,也就是不能被修改,如const char * getname()。
2.函数结尾加const表示常成员函数,表示只能读取成员变量的值,而不能修改,如char * getname() const。
在 C++ 中,const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员
(包括 const 成员变量和 const 成员函数)了。
引用:
参数的传递本质是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
c/c++禁止在函数调用时直接传递数组的内容,而是强制传递数组指针。而对于结构体和对象没有这种限制,调用函数时既可传递指针,也可直接传递内容,为提供效率,建议指针。
但是在 C++ 中,我们有了一种比指针更加便捷的传递聚合类型数据的方式,那就是引用。
引用:数据的一个别名,通过这个别名和原来的名字都能找到这份数据。
例:
#include <iostream>
using namespace std;
int main() {
int a = 99;
int &r = a;
cout << a << ", " << r << endl;
cout << &a << ", " << &r << endl;
return 0;
}
运行结果:
99, 99
0x28ff44, 0x28ff44
c++继承时的名字遮蔽问题
若派生类的成员(包括成员变量和成员函数)和基类中的成员重名,会遮蔽从基类继承过来的成员。要访问基类中的
成员函数,要加上基类类名进行访问。
类的构造函数没法被继承。
派生类中,对于继承过来的成员变量的初始化工作也得由派生类的构造函数完成,但大部分基类都有private属性的成员变量,他们在派生类中无法访问。
解决方法:在派生类的构造函数中调用基类的构造函数。
代码:
#include<iostream>
using namespace std;
//基类People
class People{
protected:
char *m_name;
int m_age;
public:
People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}
//派生类Student
class Student: public People{
private:
float m_score;
public:
Student(char *name, int age, float score);
void display();
};
//People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
}
int main(){
Student stu("小明", 16, 90.5);
stu.display();
return 0;
}
运行结果为:
小明的年龄是16,成绩是90.5。
构造函数的调用顺序:先基类,再派生类。析构相反。
基类的指针也可以指向派生类的对象。例:
#include <iostream>
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
void display();
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
People *p = new People("王志刚", 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。
我们直观认为,若指针指向了派生类对象,就应该使用派生类的成员变量和成员函数,但上例结果表明不对。
换句话:通过基类指针只能访问派生类的成员函数,不能访问派生类的成员函数。
解决:让基类指针能访问派生类的成员函数。增加虚函数。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。
#include <iostream>
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
virtual void display(); //声明为虚函数
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
Teacher(char *name, int age, int salary);
virtual void display(); //声明为虚函数
private:
int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
People *p = new People("王志刚", 23);
p -> display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
有了虚函数,基类指针指向基类对象就使用基类的成员(包括成员变量和成员函数),指向派生类对象时就使用派生类的成员。
换句话,基类指针可以按照基类的方式来做事,也可按照派生类的方式做事,有多种形态,称为多态。
引用实现多态:
int main(){
People p("王志刚", 23);
Teacher t("赵宏佳", 45, 8200);
People &rp = p;
People &rt = t;
rp.display();
rt.display();
return 0;
}
运行结果:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
构成多态条件:
1.必须存在继承关系
2.继承关系中必须有同名的虚函数,并且它们是覆盖关系
3.存在基类的指针,通过该指针调用虚函数。
构造函数,不能是虚函数,因为派生类不能继承基类的构造函数。
析构可以是虚函数。而且有时候必须声明为虚函数。
例:
#include <iostream>
using namespace std;
//基类
class Base{
public:
Base();
~Base();
protected:
char *str;
};
Base::Base(){
str = new char[100];
cout<<"Base constructor"<<endl;
}
Base::~Base(){
delete[] str;
cout<<"Base destructor"<<endl;
}
//派生类
class Derived: public Base{
public:
Derived();
~Derived();
private:
char *name;
};
Derived::Derived(){
name = new char[100];
cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
delete[] name;
cout<<"Derived destructor"<<endl;
}
int main(){
Base *pb = new Derived();
delete pb;
cout<<"-------------------"<<endl;
Derived *pd = new Derived();
delete pd;
return 0;
}
运行结果:
Base constructor
Derived constructor
Base destructor
Base constructor
Derived constructor
Derived destructor
Base destructor
本例中,不调用派生类的析构函数会导致name指向的100个char类型的内存空间得不到释放。
1.为啥delete pb,不会调用调用派生类的析构函数:
这里的析构函数是非虚函数,通过指针访问非虚函数时,会根据指针的类型来确定要调用的函数;也就是说,指针指向哪个类就调用哪个类的函数,pb是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终调用基类的析构函数。
2. 为什么delete pd;会同时调用派生类和基类的析构函数呢?
pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的。