c++语法学习

内存分区模型

  • 代码区:存放二进制代码,由操作系统管理
  • 全局区:存放全局变量、静态变量、全局常量
  • 栈区:编译器自动分配释放,存放函数参数值、局部变量等
  • 堆区:由程序员分配、释放,若不释放,结束时由操作系统回收

意义:赋予不同的生命周期,灵活编程

程序运行前

未执行.exe文件前,分为两个区域

  • 代码区:

    • 存放cpu执行的机器指令
    • 特点:共享(频繁被执行的程序只要在内存中有一份代码即可),只读(防止程序意外修改了指令)
  • 全局区:

    • 全局变量和静态变量在此

    • 包含常量区,字符串常量和其他常量也存放在此

    • 该区域数据在程序结束后由操作系统释放

    • 静态变量:在前面加static,比如static int a=10

      常量:分为全局的和局部的,但只有全局的常量在常量区,const int a=10

程序运行后

  • 栈区
    • 存放函数的局部变量及参数,编译器自动开辟与free
  • 堆区
    • 由程序员手动分配与释放,如果程序员不释放的话就由操作系统释放
    • 在c++中用new来分配内存

new操作符语法

分配:

1
2
3
4
5
int* func()
{
int* p=new int(10);//在堆中开辟了一块int大小的数据,其中的数据是10,并且new会返回分配的地址
return p;
}

释放:

1
2
3
4
5
6
7
int main()
{
int *p=func();
cout<<*p<<endl;
delete p;
cout<<*p<<endl;//这句会报错,因为用delete释放了
}

开辟一段连续的数组空间:

1
2
3
4
5
6
7
8
9
10
11
int* func2()
{
int* p=new int[10];//开辟了int类型的10个空间
//赋值
for(int i=0;i<10;i++)
p[i]=i;
//打印
for(int i=0;i<10;i++)
cout<<p[i]<<endl;
reuturn p;
}

释放数组:

1
2
3
4
5
6
int main()
{
int *p=func2();
delete[] p;//记得加中括号
reuturn 0;
}

引用

基本语法使用

  • 作用

    • 给变量起别名
  • 语法

    • 数据类型 &别名=原名

    • 比如原来有int a=10

      创建引用int &b=a;

操作的是同一块内存

注意事项

  • 必须初始化

    int &b错误的

  • 初始化后不可改变引用的对象

引用作为函数参数

  • 作用:
    • 可以用引用让形式参数修饰实参
    • 可以不用指针,简化思考

这里举一个交换两个数的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void swap(int &a,int &b)
{
int temp=a;
a=b;
b=temp;
}
int main()
{
int a=10,b=20;
cout<<"交换前:a="<<a;
cout<<" b="<<b<<endl;
swap(a,b);
cout<<"交换后:a="<<a;
cout<<" b="<<b<<endl;
return 0;
}

引用做函数返回值

  • 注意:
    • 不能返回局部变量的引用

函数调用可以作为等号左边的值

1
2
3
4
5
6
7
8
9
10
11
12
13
int &test1()
{
static int a=10;
return a;//这里返回的是a的引用
}
int main()
{
int &b=test1();
cout<<"b="<<b<<endl;
test1()=1000;//这里是对a的引用进行了修改,相当于a=1000
cout<<"b="<<b<<endl;//再用b(别名)进行访问
return 0;
}

本质

  • 本质:
    • 在C++内部就是一个指针常量,指向我引用的对象

常量引用

  • 作用:
    • 用于修饰形参,防止误操作,保护参数
1
2
3
4
5
6
7
8
9
10
11
12
13
void test1(const int &a)
{
//a=100,这里就会发生错误
//这里就可以防止这个变量的值发生改变,保护数据
cout<<a<<endl;
}
int main()
{
const int &a=10;//自己创建了一个值为10的空间,不可修改
int aa=10;
test1(a);
return 0;
}

函数提高

函数默认参数

C++中,函数的形参列表中可以有默认值

1
2
3
4
void test (int a,int b=10,int c=20)
{
...
}

如果我没有传参就用默认,如果我们有传参,就用我们传的参数

注意:

  • 如果某个位置有了默认参数,那么往后都必须要有默认值
1
2
3
4
void test (int a=10,int b,int c)
{
...
}//错误的
  • 函数的声明和实现只能一边有默认参数

函数占位参数

C++中可以有占位参数,用来占位,但在函数调用时必须填补该位置

比如

1
2
3
4
5
6
7
8
9
void test(int a,int)//第二个int就是来占位的
{
...
}
int main()
{
test(10,10);//必须填补那个占位的
return 0;
}
  • 占位参数还可以有默认参数,比如
1
2
3
4
5
6
7
8
9
void test(int a,int = 10)//第二个int就是来占位的
{
...
}
int main()
{
test(10,10);//必须填补那个占位的
return 0;
}

函数重载

概述

  • 作用:
    • 函数名可以相同,提高重用性
  • 条件:
    • 同一个作用域(比如都是全局)
    • 函数名相同
    • 函数参数类型不同个数不同顺序不同
  • 注意:
    • 函数返回值不可作为函数重载的条件

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func(int a)//参数个数不同
{
cout<<"func(int a)被调用"<<endl;
}
void func()
{
cout<<"func()被调用"<<endl;
}
int main()
{
func(10);
func();
return 0;
}

注意事项

  • 引用作为函数重载的条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func(int &a)
{
cout<<"func(int &a)被调用"<<endl;
}
void func(const int &a)
{
cout<<"func(const int &a)被调用"<<endl;
}
int main()
{
int a=10;
func(a);//调用了第一个
func(10);//调用了第二个
return 0;
}
  • 函数重载碰到默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func(int a)
{
cout<<"func(int a)被调用"<<endl;
}
void func(int a,int b=10)
{
cout<<"func(int a,int b=10)被调用"<<endl;
}
int main()
{
//func(10);//错误的
func(10,20);//正确的
return 0;
}

类和对象

C++面向对象三大特性:封装、继承、多态

封装

意义

  • 将属性和行为封装在一起,表现事物
  • 将属性和行为加以权限控制

意义1:

  • 语法:class 类名{ 属性、行为 }

比如设计一个圆类,求圆周长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle
{
//访问权限
public:
//属性
int r;
//行为
double c()
{
return 2*3.14*r;
}
};//注意这里有一个分号
int main()
{
//通过圆类创建一个对象
Circle c1;
//赋予属性
c1.r=10;
cout<<"圆周长为"<<c1.c()<<endl;
return 0;
}

类中的属性行为统一称为成员

属性:成员属性,成员变量

行为:成员函数,成员方法

意义2:

访问权限分为3种

  • public 公共权限
    • 类内类外均可访问
  • protected 保护权限
    • 类内可访问
    • 类外不可访问
    • 子可访问父
  • private 私有权限
    • 类内可访问
    • 类外不可访问
    • 子不可访问父亲

struct class区别

唯一区别:默认访问权限不同

struct:公共

class:私有

将成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限

优点2:对于写权限,我们可以检查数据有效性(就是在写的时候加if来判断即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class person
{
public:
void setname(string n)
{
name=n;
}//这样就是可读可写
void getage()
{
cout<<"年龄为:"<<age<<endl;
}这样就是只读了
string setidol(string i)
{
idol=i;
}//这样就是只写了
private:
string name;//姓名,可读可写
int age=18;//年龄 只读
srring idol;//偶像 只写
}

对象的初始化和清理

构造函数和析构函数

初始化清理是非常重要的安全问题

  • 没有初始化,对象使用后果是位置的
  • 使用完对象或变量没有及时清理,也会造成安全问题

构造和析构就是来解决初始化和清理的,由编译器自动调用,如果我们不提供构造和析构,编译器会提供,但编译器提供的是空的

  • 构造:主要作用在创建对象时为成员属性赋值,由编译器自动调用,无须手动
  • 析构:主要作用在于对象销毁前自动调用,执行一些清理

构造函数语法:类名 (){}

  • 没有返回值也不写void
  • 函数名称和类名相同
  • 可以有参数,因此可以发生重载
  • 程序在调用对象的时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法:~类名 (){}

  • 没有返回值但也不写void
  • 函数名称和类名相同,在名称前面加~
  • 不可以有参数,因此不可以发生重载
  • 程序在对象销毁前会自动调用析构,无须手动调用且只会调用一次

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class person
{
public:
person()//构造函数
{
cout<<"Person构造函数被调用"<<endl;
}
~person()//析构函数
{
cout<<"person析构函数被调用"<<endl;
}
}
void test1()
{
person p;//栈上的数据,执行完毕后会释放这个对象,也就会执行析构函数
}
int main()
{
test1();
return 0;
}

构造函数的分类及调用

分类:

  • 按参数
    • 有参构造和无参构造
  • 按类型
    • 普通构造和拷贝构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class person
{
public:
person()//无参构造函数
{
cout<<"Person无参构造函数被调用"<<endl;
}
person(int a)//有参构造函数
{
cout<<"Person有参构造函数被调用"<<endl;
}
//拷贝构造函数
//将传入的对象身上的属性拷贝到我当前的对象上
person(const person &p)
{
cout<<"Person拷贝"<<endl;
age=p.age
}
~person()//析构函数
{
cout<<"person析构函数被调用"<<endl;
}
int a;
};

调用方式:

  • 括号法
    • 注意事项
      • 调用默认构造函数时不要加(),person p1()会被当做是一个函数的声明
  • 显示法
    • 不要利用拷贝构造函数初始化匿名对象
  • 隐式转换法
1
2
3
4
5
6
7
8
9
10
11
12
13
void test01()
{
//括号法
person p;//默认构造函数调用
person p1(10);//括号法调用有参
person p2(p1);//括号法调用拷贝
//显示法
person p3=person(10);//显示法调用有参构造
person p4=person(p1);//显示法调用拷贝构造
person(10);//匿名对象,当前执行结束后会立即释放掉
//隐式转换法
person p5=10;//相当于person p5=person(10)
}

拷贝构造函数调用时机

调用的三种情况

  • 调用一个已经创建完毕的对象来初始化新对象
  • 值传递方式给函数参数传值
  • 以值传递方式返回局部对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class person
{
public:
person()
{
cout<<"默认构造函数被调用"<<endl;
}
person(int a)
{
age=a;
cout<<"有参构造被调用"<<endl;
}
~person()
{
cout<<"析构函数被调用"<<endl;
}
person(const person &p)
{
age=p.age;
cout<<"拷贝构造被调用"<<endl
}
};
void work1(person p)
{

}
person work2()
{
person p1;
return p1;
}
int main()
{
person p1(20);
person p2(p1);//第一种调用情况
work1(p1);//第二种调用情况,拷贝出临时的副本给函数用
person p=work2();//第三种调用情况,return时会拷贝一个新对象给p
return 0;
}

构造函数调用规则

默认情况下,c++编译器至少给一个类添加三个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝函数,对属性进行值拷贝

构造函数调用规则:

  • 用户自定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝函数
  • 用户自定义拷贝构造函数,C++不再提供其他的构造函数

深拷贝和浅拷贝

浅拷贝:简单赋值操作

深拷贝:在堆区重新申请空间,进行拷贝操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class person
{
public:
person()
{
cout<<"默认构造函数被调用"<<endl;
}
person(int a,int h)
{
age=a;
high=new int(h);
cout<<"有参构造被调用"<<endl;
}
~person()
{
//析构函数,将堆区开辟的数据释放掉
if (high!=NULL)
delete high;
cout<<"析构函数被调用"<<endl;
}
private:
int age;
int* high=NULL;
};

浅拷贝会导致一块内存重复被释放

  • int main()
    {
        person p1(18,120);
        person p2(p1);//系统提供的拷贝函数,是浅拷贝
        return 0;
    }//此时p1p2中的age存储的是同一块堆中的地址,当p2结束后会执行一次析构函数,此时high已经被释放掉,而p1结束后还会再来一次析构函数,而此时已经释放掉的内存就会再一次被释放,导致程序出错
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    此时就需要深拷贝,需要重新申请一块内存

    ```c++
    class person
    {
    public:
    person()
    {
    cout<<"默认构造函数被调用"<<endl;
    }
    person(int a,int h)
    {
    age=a;
    high=new int(h);
    cout<<"有参构造被调用"<<endl;
    }
    ~person()
    {
    //析构函数,将堆区开辟的数据释放掉
    if (high!=NULL)
    delete high;
    cout<<"析构函数被调用"<<endl;
    }
    //深拷贝
    person(const person &p)
    {
    cout<<"深拷贝函数被调用"<<endl;
    age=p.age;
    //high=p.high; 编译器默认提供的拷贝函数就是这句代码
    high=new int(*p.high);
    }
    private:
    int age;
    int* high=NULL;
    };

初始化列表

作用

C++提供初始化列表语法,用于初始化属性

语法:构造函数():属性1(值1),属性2(值2)....{}

参数非常数时:person(int a,int b,int c):m_A(a),m_B(b),m_C(c){}

类对象作为类成员

C++中类中的成员可以是另一个类的对象,我们称该成员为:对象成员

比如

1
2
3
4
5
6
class A{
};
class B
{
A a;
};

B中有对象A作为成员,A是对象成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class phone
{
public:
phone(string p)
{
pname=p;
}
string pname;
}
class person
{
public:
person(string n,string pn)
{
name=n;
m_phone.pname=pn;
}
srring name;
phone m_phone;
}
void test01()
{
person p("pp","apple");

}

结论:当其他类对象作为本类对象,其他类的构造函数先被调用,然后自身的构造函数再来,析构则是反过来

静态成员

在成员变量和成员函数前加static,称为静态成员

静态成员分为

静态成员变量

  • 所有对象共用一份数据
  • 编译阶段分配内存
  • 类内声明,类外初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class person
{
public:
static int a;
};
int person::a=100;//类外初始化
void test01()
{
person p;
cout<<p.a<<endl;
person p1;
p1.a=200;
cout<<p.a<<endl;
}
//输出:
//100
//200

静态成员变量不属于某个对象上,所有对象共享同一份数据

因此有两种访问方式

1
2
3
4
5
6
7
8
void test02()
{
person p;
//通过对象访问
cout<<p.a<<endl;
//通过类名访问
cout<<person::a<<endl;
}

静态成员变量同样存在访问权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class person
{
public:
static int a;
private:
static int b;
};
int person::a=100;//类外初始化
int person::b=101;
void test01()
{
person p;
cout<<p.a<<endl;
person p1;
p1.a=200;
cout<<p.a<<endl;
}
//输出:
//100
//200
void test02()
{
cout<<person::b<<endl;//会报错,由于private权限,类外不可访问
}

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量(因为无法区分这个变量是谁的)
1
2
3
4
5
6
7
8
class person
{
public:
static void func()
{
cout<<"静态成员函数被调用"<<endl;
}
};

访问方式:

  • 通过对象访问
  • 通过类名访问
1
2
3
4
5
6
7
8
void test01()
{
person p;
//通过对象访问
p.func();
//通过类名访问
person::func();
}

c++对象模型和this指针

成员变量和成员函数分开存储

C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上。静态成员变量、非静态成员函数、静态成员函数都不属于类的对象上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class person
{

};
class person1
{
int a;
};
class person2
{
int a;
static int b;
};
int person2::b=100;
void test01()
{
person p;
cout<<"size of p = "<<sizeof(p)<<endl;//输出结果是1,所以空对象占用内存是1
//c++会给每个空对象也分配一个字节,为了区别空对象占内存的位置,空对象也有独一无二的内存地址
}
void test02()
{
person1 p;
cout<<"size of p = "<<sizeof(p)<<endl;//结果是4
}
void test03()
{
person2 p;
cout<<"size of p = "<<sizeof(p)<<endl;//结果是还是4,它没有存储在类中
}

this指针

C++通过提供特殊的对象指针,this指针,来区分是哪个对象调用的。this指针指向被调用的成员函数所属的对象

  • this指针隐含在每一个非静态成员函数内的一种指针
  • this指针不需要定义,可以直接使用

用途

  • 当形参和成员函数重名时,用this指针来区分
  • 在非静态成员函数中返回对象本身,可以使用return *this

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class person
{
public:
person(int age)
{
this->age=age;
//不能直接age=age,会报错
}
person& addage(person& p)//要是引用,如果是person,返回的话就是浅拷贝创建了一个新对象,不再是原本的那个
{
this->age+=p.age;
return *this;//这样就可以重复调用
}
int age;
}
//解决名称冲突
void test01()
{
person p1(18);
cout<<"p1年龄:"<<endl;//此时this指向p1;
}
//返回对象本身用*this
void test02()
{
person p1(10);
person p2(10);
p2.addage(p1).addage(p1),addage(p1);//如果没有return *this就会报错
//链式编程思想
cout<<"p2年龄为:"<<p2.age<<endl;
}

空指针访问成员函数

C++中空指针也可以调用成员函数的,但要注意有没有用到this指针

如果遇到this指针要加以判断保证代码健壮性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class person
{
public:
void showtype()
{
cout<<"This is person class"<<endl;
}
void showage()
{
if (this==NULL)
return;//检查保证程序不会崩溃
cout<<age<<endl;
}
private:
int age;
};
void test01()
{
preson *p=NULL;
p->showtype();//这句不会
p->showage();//这句会出错,报错原因在于传入指针为空,age相当于this->age,而this指针是空
}

const修饰成员函数

  • 常函数
    • 加const后称为常函数
    • 常函数内不可修改成员属性
    • 成员属性后加mutable后,在常函数内可以修改
  • 常对象
    • 声明对象前加const后称为常函数
    • 常对象只能调用常函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class person
{
private:
int a;
public:
void showperson () const
{
a = 100;//会报错,因为这个const将this指针变成const person * const this这样左值就不可修改了
//在成员函数后加const,修饰的是this指针,让指针指向的值也不可被修改
}
void show()
{

}
};
void test01()
{
person p;
p.showperson();
}
void test02()
{
const person p;
p.a=100;//报错,也不可修改属性,但加mutable就可以修改了
p.showperson();
p.show();//会报错,不可调用普通成员函数,因为普通成员函数可以修改属性,与常对象矛盾
}

调用规则

  • const 成员函数 可以被 const 对象调用,也可以被非 const 对象调用(但优先调用非 const 版本)。
  • ** const 成员函数** 只能被非 const 对象调用。
  • const 可以用来区分重载函数,只要它们的参数列表相同,但 const 属性不同。

友元

在程序里,有些私有属性,也想让类外的一些特殊的函数或类进行访问,需要用到友元

关键字:friend

  • 三种实现
    • 全局函数做友元
    • 类做友元
    • 成员函数做友元

全局函数做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class room
{
friend void gfriend(room* r);//实现友元,这样公有和私有都可以访问了
public:
room()
{
sroom = "a";
bedroom = "b";
}
public:
string sroom;
private:
string bedroom;
};
void gfriend(room* r)
{
cout << "全局函数访问:" << r->sroom << endl;
cout << "全局函数访问:" << r->bedroom << endl;
}
void test01()
{
room rr;
gfriend(&rr);
}

类做友元

让一个类可以访问另一个类的私有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class room
{
friend class gfriend;//实现类做友元
public:
room();
public:
string sroom;
private:
string bedroom;
};
room::room()//类外实现构造函数
{
sroom = "a";
bedroom = "b";
}
class gfriend
{
public:
room* r;
gfriend()
{
r = new room;
}
void visit()
{
cout << "gfriend正在访问:" << r->sroom << endl;
cout << "gfriend正在访问:" << r->bedroom << endl;
}
};
void test01()
{
gfriend gg;
gg.visit();
}

成员函数做友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class room; // 前向声明

class gfriend
{
public:
room* r;
gfriend();
void visit(); // 先声明成员函数
};

class room
{
friend void gfriend::visit(); // 此时 gfriend 类已定义,实现友元
public:
room();
string sroom;
private:
string bedroom;
};

// 类成员函数的实现
room::room()
{
sroom = "a";
bedroom = "b";
}

gfriend::gfriend()
{
r = new room;
}

void gfriend::visit()
{
cout << "gfriend正在访问:" << r->sroom << endl;
cout << "gfriend正在访问:" << r->bedroom << endl; // 现在可以访问私有成员
}

void test01()
{
gfriend gg;
gg.visit();
}

运算符重载

对运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

加号运算符重载

作用:实现自定义数据类型的运算

成员函数重载加号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class person
{
public:
int a;
int b;
person operator+(person& p)
{
person temp;
temp.a = a + p.a;
temp.b = b + p.b;
return temp;
}
};
void test01()
{
person p1;
p1.a = 10;
p1.b = 10;
person p2;
p2.a = 10;
p2.b = 10;
person p3 = p1 + p2;
//本质p3=p1.operator+(p2);简化为上面的写法
cout << "p3.a=" << p3.a << endl;
cout << "p3.b=" << p3.b << endl;
}

全局函数重载加号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class person
{
public:
int a;
int b;
};
person operator+(person& p1, person& p2)
{
person temp;
temp.a = p1.a + p2.a;
temp.b = p2.b + p2.b;
return temp;
}
void test01()
{
person p1;
p1.a = 10;
p1.b = 10;
person p2;
p2.a = 10;
p2.b = 10;
person p3 = p1 + p2;
//本质p3=operatoe(p1,p2)简化为上面的写法
cout << "p3.a=" << p3.a << endl;
cout << "p3.b=" << p3.b << endl;
}

运算符重载发生函数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class person
{
public:
int a;
int b;
};
person operator+(person& p1, person& p2)
{
person temp;
temp.a = p1.a + p2.a;
temp.b = p2.b + p2.b;
return temp;
}
person operator+(person& p1, int n)
{
person temp;
temp.a = p1.a + n;
temp.b = p1.b + n;
return temp;
}
void test01()
{
person p1;
p1.a = 10;
p1.b = 10;
person p2;
p2.a = 10;
p2.b = 10;
person p3 = p1 + p2;
person p4 = p1 + 20;
//本质p3=operatoe(p1,p2)简化为上面的写法
cout << "p3.a=" << p3.a << endl;
cout << "p3.b=" << p3.b << endl;
cout << endl;
cout << "p4.a=" << p4.a << endl;
cout << "p4.b=" << p4.b << endl;
}

总结1:对于内置的数据类型的表达式的运算符不能重载

总结2:不要滥用运算符重载(把加法写成减法等等)

左移运算符重载

作用:输出自定义数据类型<<

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class person
{
friend ostream& operator<<(ostream& cout, person& p);
friend void test01();
//成员函数重载
//void operator<<(cout)
//{
//}
//一般不会用成员函数重载<<,应为无法实现cout在左侧
private://一般设置为私有,所以要设置友元
int a;
int b;
};
//只能利用全局函数重载左移运算符
ostream& operator<<(ostream& cout, person& p)//本质 operator<<(cout,p),简化后cout<<p
{
cout << "a=" << p.a << endl;
cout << "b=" << p.b << endl;
return cout;//这样就能继续输出endl,否则只能cout<<p;
}
void test01()
{
person p;
p.a = 10;
p.b = 10;
cout << p << endl;
}

递增运算符重载

实现前置递增和后置递增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class person
{
friend ostream& operator<<(ostream& cout,const person& p);
public:
person()
{
a = 0;
}
//重载前置++
person& operator++()
{
a++;
return *this;//不能返回值,否则是拷贝,两次++不会更改原本的值,返回引用是为了一直对一个数据做递增
}
//重载后置++
person operator++(int)//int是占位参数,用来区分前置和后置
{
person temp = *this;
a++;
return temp;//这时返回的是值而不是引用,因为这是对局部变量的引用,会报错
}
private:
int a;
};
ostream& operator<<(ostream& cout,const person& p)//要加const,否则不能接收临时对象,也就是p+
{
cout << p.a;
return cout;
}
void test01()
{
person p;
cout << p << endl;
cout << ++p << endl;
cout << p << endl;
cout << p++ << endl;
cout << p;
}

总结:前置递增返回的是引用,后置递增返回的是值

赋值运算符重载

C++至少给一个类添加4个函数

  • 默认构造函数
  • 默认析构函数
  • 默认拷贝函数
  • 赋值运算符operator=,对属性进行值拷贝

如果有属性指向堆区,也会出现深浅拷贝的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class person
{
public:
person(int ag)
{
age = new int(ag);
}
~person()
{
if (age != NULL)
{
delete age;
age = NULL;
}
}
//重载赋值运算符
person& operator=(person& p)
{
//编译器提供的是浅拷贝,我们要写的是深拷贝
//先判断是否有属性在堆上,如果有的话先释放干净
{
if (age != NULL)
{
delete age;
age = NULL;
}
}
//深拷贝
age = new int(*p.age);
return *this;//可以进行连等,p3=p2=p1;
}
int* age;
};
void test01()
{
person p1(18);
person p2(20);
person p3(22);
p3 = p2 = p1;
cout << "p1=" << *p1.age << endl;
cout << "p2=" << *p2.age << endl;
cout << "p3=" << *p3.age << endl;
}

关系运算符重载

作用:可以让两个自定义数据类型可以进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class person
{
public:
person(int a)
{
age=a;
}
int age;
bool operator==(person& p)
{
if (age==p.age)
return true;
else
return false;
}
};
void test01()
{
person p1(18);
person p2(18);
if (p1==p2)
cout<<"p1与p2相等"<<endl;
else
cout<<"p1与p2不相等"<<endl;
}

其他符号都与上面这个类似,只要改几个符号即可,这里只写了==

函数调用运算符的重载

  • 函数运算符()也可以重载
  • 重载后使用方式和函数非常像,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<iostream>
using namespace std;
class mprint
{
public:
//重载函数调用运算符
void operator()(string test)
{
cout << test << endl;
}
};
class madd
{
public:
int operator()(int n1, int n2)
{
return n1 + n2;
}
};
void test01()
{
mprint mp;
mp("Hello World!");
}
void test02()
{
madd md;
cout << md(100, 10) << endl;
//匿名函数对象(执行完立即被释放)
cout << madd()(100, 20) << endl;
}
int main()
{
test01();
test02();
return 0;
}

继承

面向对象的三大特性之一

继承

我们发现在定义这些类时,下级别除了拥有上一级的共性,还有自己的特性

这里我们就可以利用继承来减少代码量

继承基本语法

  • 语法:class 子类 : 继承方式 父类

  • 子类也称派生类

  • 父类也称基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//继承实现
//公共页面类
class basepage
{
public:
void header()
{
cout << "登陆 注册" << endl;
}
};
//Java页面
class Java : public basepage
{
public:
void content()
{
cout << "Java" << endl;
cout << "-----------------" << endl;
}
};
class python : public basepage
{
public:
void content()
{
cout << "python" << endl;
cout << "-----------------" << endl;
}
};
class cpp : public basepage
{
public:
void content()
{
cout << "C++" << endl;
cout << "-----------------" << endl;
}
};
void test01()
{
Java ja;
ja.header();
ja.content();
python py;
py.header();
py.content();
cpp cjj;
cjj.header();
cjj.content();
}

效果

总结:继承减少重复的代码

继承方式

  • 公共继承
  • 保护继承
  • 私有继承

继承方式

父类私有成员子类访问不到

  • 公共继承
    • 继承父类中的各种属性的权限
  • 保护继承
    • 将公共权限变为保护,其它不变,类内可访问类外不可访问
  • 私有继承
    • 全部变成private,类外全部不能访问
    • 此时如果又有一个类(子子类)来继承这个子类,那么也无法访问这个子类的属性,因为子类继承父类时变成私有的了,子子类无法访问

继承中的对象模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class base
{
public:
int a;
protected:
int b;
private:
int c
};
class son:puiblic base
{
public:
int d;
};
test01()
{
cout<<"size: "<<sizeof(son)<<endl;
}
//结果是16

结论:

  • 父类中所有非静态成员都会被子类继承
  • 私有属性也有被继承只不过被隐藏了无法访问而已

继承中构造和析构函数

结论:

  • 父类构造
  • 子类构造
  • 子类析构
  • 父类析构

继承同名成员的处理方式

当子类与父类出现同名成员时,如何通过子类访问子类或父类中的同名成员

  • 子类中的直接访问
  • 父类通过作用域访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class base
{
public:
int a;
base()
{
a = 100;
}
};
class son :public base
{
public:
int a;
son()
{
a = 200;
}
};
void test01()
{
son s;
//访问son下的a
cout << "son下的a=" << s.a << endl;
//访问base的a
cout << "base下的a=" << s.base::a << endl;
}

函数同理

多态

C++面向对象三大特性之一

多态基本概念

分为两类

  • 静态多态:函数重载 运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时的多态

区别:

  • 静态多态的函数地址早绑定,编译阶段确定函数地址
  • 动态多态的函数地址晚绑定,允许阶段确定函数地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class animal
{
public:
void speak()
{
cout<<"animal说话"<<endl;
}
};
class cat:public animal
{
public:
void speak()
{
cout<<"cat说话"<<endl;
}
};
void dospeak(animal& ani)
{
ani.speak();
}
void test01()
{
cat c;
dospeak(c);//父子之间不用强制类型转换,父类能直接用子类的引用或者指针
}

结果:

地址早绑定,编译阶段就确定了函数的地址,无论穿什么参数,都会执行ani.speak()

如果要执行cat说话,地址就不能早绑定,需要在运行阶段绑定,也就是晚绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class animal
{
public:
//虚函数,实现地址晚绑定
virtual void speak()
{
cout<<"animal说话"<<endl;
}
};
class cat:public animal
{
public:
void speak()
{
cout<<"cat说话"<<endl;
}
};
void dospeak(animal& ani)
{
ani.speak();
}
void test01()
{
cat c;
dospeak(c);//父子之间不用强制类型转换,父类能直接用子类的引用或者指针
}

结果:

动态多态的条件:

  • 有继承关系
  • 子类重写父类的虚函数
    • 重写:函数返回值类型 函数名 参数列表完全相同

动态多态的使用:

  • 父类的指针或者引用指向子类的对象

多态的本质

  • 当父类使用virtual关键字创建虚函数的时候,其内部会有一个vfptr(virtual function prt)指针父类的虚函数表,这个时候父类的虚函数表中会有一个地址:&animal::speak

  • 当子类继承父类的时候会继承父类的vfptr指针,指向子类的虚函数表,但是当子类重写父类的虚函数的时候,会将自身的虚函数表中的地址给替换掉,替换成:&cat::speak,这样就可以调用指向cat::speak的指针

案例1:计算器类

案例描述:分别利用普通写法和多态技术,实现两个操作数进行运算的计算机类

多态优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展和维护

普通:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class cac
{
public:
int n1;
int n2;
int getresult(string oper)
{

if (oper == "+")
return n1 + n2;
else if (oper == "-")
return n1 - n2;
else if (oper == "*")
return n1 * n2;
//如果要扩展新功能,需要修改源码
//在真实开发中,提倡开闭原则:对扩展进行开放,对修改进行关闭
}
};
void test01()
{
cac c;
c.n1 = 10;
c.n2 = 10;
cout << c.getresult("+") << endl;
cout << c.getresult("-") << endl;
cout << c.getresult("*") << endl;
};

多态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class basecac
{
public:
virtual int getresult()
{
return 0;
}
int n1;
int n2;
};
//加法类
class addcac:public basecac
{
public:
int getresult()
{
return n1+n2;
}
};
//减法类
class jfcac:public basecac
{
public:
int getresult()
{
return n1-n2;
}
};
void test01()
{
basecac *a=new addcac;//用父类指针指向子类对象,发生多态
basecac *b=new jfcac;
a->n1=10;
a->n2=10;
b->n1=20;
b->n2=10;
cout<<a->getresult()<<endl;
cout<<b->getresult()<<endl;
//用完后堆区记得销毁
delete a;
delete b;
}

可见优点:

  • 组织结构清晰
  • 可读性强
  • 便于前期和后期的扩展的修改

总结:C++开发中提倡利用多态设计程序架构,其优点很多

纯虚函数和抽象类

在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是在调用子类重写的内容

因此可以将虚函数改成纯虚函数

语法:virtual 返回值类型 函数名 (参数列表)=0

当类中有一个纯虚函数,这个类也就称为抽象类

抽象类特点:

  • 无法实例化对象
  • 子类必须要重写抽象类的纯虚函数,否则这个子类也属于抽象类
1
2
3
4
5
6
7
8
9
10
class base
{
public:
virtual void func()=0;
};
class son:public base
{
public:
void func(){}//子类必须要重写父类中的纯虚函数
};

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决:将父类中析构函数改为虚析构或纯虚析构

虚析构和纯虚析构共性

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

区别:

  • 如果是纯虚析构函数,该类属于抽象类,无法实例化对象

虚析构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class animal
{
public:
animal()
{
cout<<"动物活了"<<endl;
}
virtual void speak()
{
}
virtual ~animal()//改成虚析构后在销毁父类指针后才会执行子类析构
{
cout<<"动物死了"<<endl;
}
};
class cat:public animal
{
public:
string *name=NULL;
cat(string n)
{
cout<<"猫活了"<<endl;
name=new string(n);
}
void speak()
{
cout<<"1"<<endl;
}
~cat()
{
if (name!=NULL)
{
cout<<"猫死了"<<endl;
delete name;
name=NULL;
}
}
};
void test01()
{
animal* a=new cat("aa");
a->speak;
//父类指针在析构的时候不会调用子类的析构函数
delete a;
}

纯虚析构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class animal
{
public:
animal()
{
cout<<"动物活了"<<endl;
}
virtual void speak()
{
}
// virtual ~animal()//改成虚析构后在销毁父类指针后才会执行子类析构
// {
// cout<<"动物死了"<<endl;
// }
virtual ~animal()=0;//注意纯虚析构必须要有函数实现
};
animal::~animal()//这个是必须的
{
cout<<"纯虚析构"<<endl;
}
class cat:public animal
{
public:
string *name=NULL;
cat(string n)
{
cout<<"猫活了"<<endl;
name=new string(n);
}
void speak()
{
cout<<"1"<<endl;
}
~cat()
{
if (name!=NULL)
{
cout<<"猫死了"<<endl;
delete name;
name=NULL;
}
}
};
void test01()
{
animal* a=new cat("aa");
a->speak;
//父类指针在析构的时候不会调用子类的析构函数
delete a;
}

PTA错题回顾

判断

0x001

对于已正确定义的二维数组a, *(a[i]+j)与a[i][j]的含义相同。

T F

  • 答案为T

0x002

不允许把一个数值或字符赋予指针变量。

T F

  • 答案为T

0x003

若重载为友元函数,函数定义格式如下:

<类型>operator<运算符>(<参数列表>)

{

<函数体>

}

T F

  • 答案为F

0x004

对每个可重载的运算符来讲,它既可以重载为友元函数,又可以重载为成员函数,还可以重载为非成员函数。

T F

  • 答案为F,有些运算符必须要用全局函数来重载,比如说左移(<<)

0x005

重定义虚函数的派生类必须是公有继承的。

T F

  • T

0x006

如果一个类的函数全部都是纯虚函数,则这个类不能有自己类的实现(包括引用和指针),只能通过派生类继承实现。

T F

  • F

0x007

虚函数既可以在函数说明时定义,也可以在函数实现时定义。

T F

  • F

0x008

含有纯虚函数的类是不可以用来创建对象的,因为它是虚基类。

T F

  • F,是抽象类

选择

0x001

执行以下的程序片段,将输出几个数字?

1
2
3
for(i=0;i<3;i++);

cout<<i;

A.0B.3C.2D.1

  • 答案为D
    • for循环后有分号,cout不在循环体内

0x002

以下数组定义中错误的是( )。

A.
int x[2][3]={{1,2},{3,4},{5,6)};

B.
int x[][3]={{1,2,3},(4,5,6)};

C.
int x[][3]={0};

D.
int x[2][3]={1,2,3,4,5,6};

  • 答案为A,定义的数组是两行三列的,而给出的数据是三行两列的

0x003

设变量定义为 int a[2]={1,3}, *p=&a[0]+1;,则*p的值是( )。

A.&a[0]+1B.2C.4D.3

  • 答案为D,*p代表地址上的值

0x004

对类的构造函数和析构函数描述正确的是( )。

A.构造函数不能重载,析构函数也不能重裁

B.构造函数不能重载,析构函数可以重载

C.构造函数可以重载,析构函数不能重载

D.构造函数可以重载,析构函数也可以重载

  • 答案为C

0x005

下列程序的运行结果是()。

1
2
3
4
5
6
7
8
int i;
for(i=0;i<3;i++)
switch(i)
{
case 1:cout<<i;
case 2:cout<<i;
default:cout<<i;
}

A.012

B.011122

C.012020

D.120

  • 答案为B,因为case里面没有break

0x006

一个函数为void f(int x, char y = 'a'),另一个函数为void f(int),则它们_

A.不能在同一程序块中定义

B.可以在同一个程序块中定义,但不可以重载

C.以上说法均不正确

D.可以在同一个程序块中定义并可重载

  • 答案为D

0x007

关于类模板,描述错误的是。

A.一个普通基类不能派生类模板

B.类模板可以从普通类派生,也可以从类模板派生

C.根据建立对象时的实际数据类型,编译器把类模板实例化为模板类

D.函数的类模板参数需生成模板类并通过构造函数实例化

  • 答案A

0x008

类模板的参数错误的说法是:

A.可以有多个

B.可以有0个

C.参数不能给初值

D.不能有基本数据类型

  • 答案

0x009

若需要为xv类重载乘法运算符,运算结果为xv类型,在将其声明为类的成员函数时,下列原型声明正确的是_________。

A.xv operator*(xv);

B.operator*(xv);

C.xv*(xv);*

D.xv operator*(xv,xv);

  • 答案为A

0x00A

继承机制的作用是

A.数据封装B.数据抽象C.信息隐藏D.定义新类

  • 答案为D

0x00B

一个类的私有成员

A.只能被该类的成员函数和友元函数访问

B.以上答案都不对

C.只能被该类的成员函数访问

D.只能被该类的成员函数、友元函数和派生类访问

  • 答案为A

函数

程序

0x1

R7-1 定义类模板实现2个数的算术运算

分数 20

全屏浏览切换布局

作者 孙杰

单位 青岛大学

本题目要求定义类模板实现2个数的最大值、最小值、加、减、乘、除等算术运算,在main()函数中使用该类模板分别实例化为int型和double型的类,定义相关的对象,读入2个整数和2个浮点数,然后分别输出它们的最大值、最小值、加、减、乘、除的结果。

输入格式:

分别输入2组数字,第一行为2个整数,以空格分隔;第二行为2个浮点数,以空格分隔。

输出格式:

分2行分别输出整数和浮点数的运算结果,每行依次输出2个数的最大值、最小值、加、减、乘、除等算术运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
using namespace std;
template <typename T>
class Arithmetic
{
private:
T num1;
T num2;
public:
Arithmetic(T a, T b)
{
num1=a;
num2=b;
}
T getMax()
{
if (num1>num2)
return num1;
else
return num2;
}
T getMin()
{
if (num1<num2)
return num1;
else
return num2;
}
T add()
{
return num1 + num2;
}
T subtract()
{
return num1 - num2;
}
T multiply()
{
return num1 * num2;
}
T divide()
{
return num1 / num2;
}
void pprint()
{
cout << getMax() << " " << getMin() << " "
<< add() << " " << subtract() << " "
<< multiply() << " " << divide();
}
};

int main() {
int int1, int2;
cin >> int1 >> int2;
Arithmetic<int> intArith(int1, int2);
double double1, double2;
cin >> double1 >> double2;
Arithmetic<double> doubleArith(double1, double2);
intArith.pprint();
cout << endl;
doubleArith.pprint();
cout << endl;
return 0;
}

补充

模板的语法及使用

函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
template <typename T>
T maxx(T a,T b)
{
if (a>b)
return a;
else
return b;
}
int main()
{
int a1=maxx(3,5);
double a2=maxx(2.5,3.1);
cout<<a1<<endl;
cout<<a2<<endl;
return 0;
}

类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
using namespace std;
template <typename T>
class Box {
public:
T content;
Box(T c) : content(c) {}
void speak()
{
cout<<content<<endl;
}
};

// 使用
int main()
{
Box<int> intBox(42);
Box<std::string> strBox("Hello");//需要显示说明类型
intBox.speak();
strBox.speak();
return 0;
}

单向链表的创建及删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct ListNode *readlist()//按输入顺序创建链表
{
struct ListNode* head=NULL;
struct ListNode* tail=NULL;
int a;
while (1)
{
scanf("%d",&a);
if (a==-1)
break;
struct ListNode* new=(struct LiseNode*)malloc(sizeof(struct ListNode));
new->data=a;
new->next=NULL;
if (head==NULL)
head=new;
else
tail->next=new;
tail=new;
}
return head;
}
struct ListNode *deletem(struct ListNode *L, int m) //删除链表中值=m的节点
{
struct ListNode dummy; // 创建一个哑节点
dummy.next = L; // 哑节点的 next 指向链表头
struct ListNode *prev = &dummy; // prev 指向哑节点

while (prev->next != NULL)
{
if (prev->next->data == m)
{
struct ListNode *temp = prev->next;
prev->next = temp->next;
free(temp);
}
else
prev = prev->next;
}
return dummy.next;
}

c++语法学习
http://yyyffff.github.io/2025/03/19/c++语法学习/
作者
yyyffff
发布于
2025年3月19日
许可协议