c++查漏补缺

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指向的100char类型的内存空间得不到释放。
1.为啥delete pb,不会调用调用派生类的析构函数:
这里的析构函数是非虚函数,通过指针访问非虚函数时,会根据指针的类型来确定要调用的函数;也就是说,指针指向哪个类就调用哪个类的函数,pb是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终调用基类的析构函数。

2. 为什么delete pd;会同时调用派生类和基类的析构函数呢?
pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的。


虚函数声明为纯虚函数,语法:
virtual 返回值类型 函数名 (函数参数) = 0;

包含纯虚函数的类称为抽象类。无法实现实例化,也就是无法创建对象。
抽象类通常是基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
当通过指针访问类的成员函数时:
1.该函数是非虚函数,那么编译器会根据指针的类型找到该函数;也就是说,指针指向哪个类的类型就调用哪个类的函数。
2.该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数。也就是说,指针指向的对象属于哪个类就调用哪个类的函数。这就是多态。

编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。

如果一个类包含了虚函数,那么在创建该类的时候就额外地增加了一个数组,数组中的每一个元素都是虚函数的入口地址。
CPU访问内存时需要的是地址,而不是变量名和函数名。变量名和函数名只是地址的一种助记符,当源文件被编译和链接程可执行
程序后,他们会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
对象的创建包括两个阶段,首先要分配内存空间,然后再初始化:
1.分配内存很好理解,就是在堆区,栈区或全局数据区留出足够多的字节。
2.初始化就是首次对内存赋值,让它的数据有意义。
注:首次赋值,再次赋值不叫初始化。

很明显,这里所说的拷贝是在初始化阶段进行的,也就是用其他对象的数据来初始化新对象的内存。
代码:
#include <iostream>
#include <string>
using namespace std;
void func(string str){
    cout<<str<<endl;
}
int main(){
    string s1 = "http://c.biancheng.net";
    string s2(s1);
    string s3 = s1;
    string s4 = s1 + " " + s2;
    func(s1);
    cout<<s1<<endl<<s2<<endl<<s3<<endl<<s4<<endl;
   
    return 0;
}

运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net http://c.biancheng.net

S1,S2,S3,S4以及func()的形参str,都是使用拷贝的方式来初始化的。
对于func()的形参str,其实在定义时就为它分配了内存,但此时并没有初始化,只有等到调用func()时,才会将其他对象的数据拷贝给str以完成初始化。
当以拷贝的方式初始化一个对象时,会调用一个特殊的构造函数,就是拷贝构造函数。
代码:
#include <iostream>
#include <string>
using namespace std;
class Student{
public:
    Student(string name = "", int age = 0, float score = 0.0f);  //普通构造函数
    Student(const Student &stu);  //拷贝构造函数(声明)
public:
    void display();
private:
    string m_name;
    int m_age;
    float m_score;
};
Student::Student(string name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//拷贝构造函数(定义)
Student::Student(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
   
    cout<<"Copy constructor was called."<<endl;
}
void Student::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
int main(){
    Student stu1("小明", 16, 90.5);
    Student stu2 = stu1;  //调用拷贝构造函数
    Student stu3(stu1);  //调用拷贝构造函数
    stu1.display();
    stu2.display();
    stu3.display();
   
    return 0;
}

运行结果:
Copy constructor was called.
Copy constructor was called.
小明的年龄是16,成绩是90.5
小明的年龄是16,成绩是90.5
小明的年龄是16,成绩是90.5

1.为什么必须是当前类的引用?
如果拷贝构造函数的参数不是当前类的 引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个
对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,
将继续调用拷贝构造函数,就会一直持续下去,陷入死循环。

2.为啥const引用?
拷贝构造函数的目的是用其他对象的数据来初始化当前对象,并没有期望更改其他对象的数据,加const后,更加明确了。
class Base{
public:
    Base(): m_a(0), m_b(0){ }
    Base(int a, int b): m_a(a), m_b(b){ }
private:
    int m_a;
    int m_b;
};
int main(){
    int a = 10;
    int b = a;  //拷贝
    Base obj1(10, 20);
    Base obj2 = obj1;  //拷贝
    return 0;
}

b 和 obj2 都是以拷贝的方式初始化的,具体来说,就是将 a 和 obj1 所在内存中的数据按照二进制位
(Bit)复制到 b 和 obj2 所在的内存,这种默认的拷贝行为就是浅拷贝,这和调用 memcpy() 函数的效果非常类似。

对于简单的类,默认的拷贝构造函数一般就够用了。
当类有动态分配的内存,指向其他数据的指针等,默认拷贝构造函数就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,
来完整地拷贝对象的所有数据。
代码:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值