Skip to content

Monday, 4 March 2024

C++初始

1.编写C++程序步骤

  • 创建项目
  • 创建文件
  • 编写代码
  • 运行程序

1.3变量

作用:给一段指定的内存空间起名,方便操作这段内存

语法:数据类型 变量名 =初始值

1.5关键字

预先保留的单词

在定义变量或者常量的时候,不要用关键字

asmdoifreturntypedef
autodynamic_castinlineshorttypeid
boolelseintsignedtypename
breakenumlongsizeofunion
caseexplicitmutablestaticunsigned
catchexportnamespacestatic_castusing
charexternnewstructvirtual
classfalseoperatorswitchvoid
constfloatprivatetemplatevolatile
const_castforprotectedthiswchar_t
continuefriendpublicthrowwhile
defaultgotoregistertrue
deletedoublereinterpret_casttry

标识符命名规则

  • 标识符不能是关键字

  • 标识符只能由字母,数字,下划线组成

  • 第一个字符必须为字母或者下划线

  • 标识符中区分大小写

    给标识符命名时,争取做到见名知意的效果,方便阅读

数据类型

实型

作用:用于表示小数

浮点型变量分为两种:

  1. 单精度float
  2. 双精度double

两者的区别在与表示的有效数字范围不同.

数据类型占用空间有效数字范围
float4字节7位有效数字
double8字节15 ~ 16位有效数字

也可以用科学计数法表示.

字符型

作用:字符型变量用于显示单个字符

语法char ch = 'a';

注意1:在显示字符型变量时,用单引导将字符括起来,不要用双引号

注意2:单引号内只能有一个字符,不可以是字符串

字符串型

1
2
3
4
5
//c风格的字符串
char str[]="helloworld";
//c++风格的字符串
//要包含头文件
string str="helloworld";

布尔类型

本质上1代表真,0代表假

占一个字节.

数据的输入

关键字cin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//整形
int a =0;
cout << "请输入整形变量:" << endl;
cin >> a;
cout << a << endl;

//浮点型
double a =0;
cout << "请输入浮点型变量:" << endl;
cin >> a;
cout << a << endl;

//字符型
char a =0;
cout << "请输入字符型变量:" << endl;
cin >> a;
cout << a << endl;

//字符串型
string a =0;
cout << "请输入字符串型变量:" << endl;
cin >> a;
cout << a << endl;

小数间是不能做取模运算的

程序流程结构

c/c++支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构

  • 顺序结构:程序按顺序执行,不发生跳转
  • 选择结构:依据条件是否满足,有选择的执行相应功能
  • 循环结构:依据条件是否满足,循环多次执行某段代码

跳转语句

break语句

作用:用于跳出选择结构或者循环结构

break使用的时机:

  • 出现在switch条件语句中,作用是终止case并跳出switch
  • 出现在循环语句中,作用是跳出当前的循环语句
  • 出现在嵌套循环中,跳出最近的内存循环语句

continue 语句

作用:在循环语句中,跳出本次循环中余下尚未执行的语句,继续执行下一次循环

函数

作用:将一段经常使用的代码封装起来,减少重复的代码

函数的定义一般主要有5步骤:

  1. 返回值类型
  2. 函数名
  3. 参数列表
  4. 函数体语句
  5. return 表达式

指针

空指针:指针变量指向内存中编号为0的空间

用途:初始化指针变量

注意:空指针指向的内存是不可以访问的

结构体

核心编程

内存分区模型

  • 代码区 存放CPU执行的机器指令 共享 只读

  • 全局区

    局部变量、const修饰的局部变量(局部变量)不在全局区中
    全局变量 静态变量 static关键字 常量全局区
  • 栈区

    不要返回局部变量的地址

  • 堆区

    在c++中主要利用new在堆区开辟内存

意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程

堆区

​C++中利用new操作符在堆区开辟数据

堆区开辟的数据由程序员手动开辟、手动释放、释放利用操作符delete

语法:new 数据类型

利用new创建的数据,会返回该数据对应的类型的指针

new的基本语法

1
int *a=new int(10);

引用

作用:给变量起别名

语法:数据类型 &别名 =原名

注意事项

  • 引用必须要初始化
  • 引用一旦初始化后,就不可以更改

引用可以简化指针的用法.

引用做函数参数

作用:函数传参时,可以利用引用的技术让形参修饰实参

优点:可以简化指针修改实参

通过引用参数产生的效果按地址传递是一样的。引用的语法更加的简单。

引用是可以作为函数的返回值存在的.

不返回局部变量的引用.

函数的调用可以作为左值.

1
2
3
int &ref2=test02();

test02()=1000;

本质:引用的本质在c++内部实现是一个指针常量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func(int &ref)
{
ref=100;
}

int main()
{
int a=10;
//自动转换为int *const ref = &a;指针常量是指针指向不可改,也说明为什么引用不可以改
int &ref=a;
ref=20;

func(a);
return 0;
}

引用必须引一块合法的内存空间。

const修饰的局部变量不再全局区.

函数

函数默认参数

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

语法 :返回值类型 函数名 (参数= 默认值) {}

1
2
3
4
5
6
int func(int a,int b=10,int c=10)
{
return a+b+c;
}
//1.如果某个位置参数有默认值,那么这个位置往后,从左向右,必须都要有默认值
//2.如果函数声明有默认值,函数实现的时候就不能有默认值

函数的默认参数,如果我们传入了参数,值就使用我们传入的参数,如果我们没有传入参数,那么就使用函数的默认参数值

注意事项:

  • 如果某个位置已经有了默认参数,那么这个位置往后,从左到右都必须要有默认值
  • 如果函数声明有默认参数,函数实现就不能有默认参数
  • 声明和实现只能一个有默认参数

函数占位参数

c++中的函数的形参列表可以有占位参数,用来做占位,调用函数时必须填补该位置

语法:返回值类型 函数名 (数据类型) { }

目前占位参数我们还用不到.

占位参数还可以有默认参数.

函数重载

作用:函数名可以相同,提高复用性

满足条件

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同 或者 个数不同 或者顺序不同

注意事项:

  • 函数的返回值不可以作为函数重载的条件

  • 引用作为重载的条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void fun(int &a)//这两个可以作为函数重载的条件,因为函数的参数类型不同
    {
    cout << "func(int &a)调用" <<endl;
    }

    void fun(const int &a)
    {
    cout << "func(const int &a)调用"<<endl;
    }

    int main()
    {
    int a=10;
    func(a);//调用的是第一个,因为这是一个变量.
    int func(10);//调用的是第二个。
    return 0;
    }
  • 函数重载碰到默认参数(会出现二义性,尽量避免使用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void func2(int a)
    {
    cout << "func2(int a)的调用" <<endl;
    }

    void func2(int a,int b=10)
    {
    cout << "func2(int a,int b)的调用" <<endl;
    }

    int main()
    {
    func2(10);
    return 0;
    }

类和对象

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

c++认为万事万物都皆为对象,对象上有其属性和行为

具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类

封装

意义

封装是c++面向对象的三大特性之一

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

封装意义1:

在设计类的时候,属性和行为写在一起,表示事物

语法: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
const double PI =3.14;
class Circle
{
//访问权限
public:
//圆的属性
int m_r;
//行为
//获取周长
double calculateZC()
{
return 2*PI*m_r;
}
}

int main()
{
//通过圆类 创建具体的圆(对象)
//实例化(通过一个类 创建一个对象的过程)
Circle c1;
//给圆对像的属性进行赋值、
c1.m_r=10 ;
cout << c1.calculateZC()<<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
23
24
25
26
27
28
29
30
31
32
33
34
class Student//设计学生类
{
public ://公共权限
//类中的属性和行为 我们统称为 成员
//属性 也成为 成员属性 成员变量
//行为 也称为 成员函数 成员方法
//属性
string m_Name;
int m_Id;
//行为
void show()
{
cout<<"姓名: " << m_Name << "学号 :" << m_Id<<endl;
}
void set_name(string name)
{
m_Name=name;
}
void set_Id(int id)
{
m_Id=id;
}
}

int main()
{
Student s1;
cin>>s1.m_Name>>m_Id;
s1.show();
Student s1;
s2.set_name("张三");
s2.set_Id(1);
return 0;
}

封装意义二:

类在设计时,可以把属性和行为放在不同的权限下,加以控制

三种权限:

1.public 公共权限

2.protected 保护权限

3.private私有权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//访问权限 三种
//公共权限public成员 类内可以访问 类外也可以访问
//保护权限protected成员 类内可以访问 类外不可以访问
//私有权限private成员 类内可以访问 类外不可以访问
//私有权限与保护权限的区别 在继承的时候可以区分 儿子可以访问父亲中的保护内容
//儿子不可以访问父亲的私有内容
class Person
{
public:
string m_Name;
protected:
string m_Car;
private:
int m_Password;
public:
void func()
{
m_Name="张三";
m_Car="饿狼传说"
m_Password=123145;
}
};

class 和 struct 的区别

在c++中struct 和 class 唯一的区别就在于默认的访问权限不同

区别:

  • struct默认权限为公共
  • class 默认权限为私有

成员属性设置为私有

优点

  • 将所有成员属性设置为私有,可以自己控制读写权限
  • 由于写权限,我们可以检测数据的有效性

在类中可以让另外一个类作为类中本来的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point
{
public:
//在此实现成员函数
private:
int m_X;
int m_Y;
}
class circle
{
public:
//在此实现成员函数
private:
int m_R;
Point m_Center;
}
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 Point
{
public:
void setx(int x);
void sety(int y);
private:
int m_X;
int m_Y;
}

class Circle
{
public:
void setm_R(int r);
int getr();
private:
int m_R;
Point Center;
}
//在其余的文件定义该函数的时候需要加作用域
void Point:: setx(int x)
{
m_X=x;
}

对象的初始化清理

c++中的面向对象来源于生活 ,每个对象也都会有初始设置 以及对象销毁前的清理数据的设置。

构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

​一个对象或者变量没有初始状态,对其使用后果未知

​同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

c++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会给我们提供空实现

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

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

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

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前自动调用析构,无需手动调用,而且只会调用一次

构造函数的分类及调用:

  • 按参数

    • 有参构造
    • 无参构造
  • 按类型

    • 普通构造
    • 拷贝构造

三种调用方法:

  • 括号法

    1
    2
    3
    Person P;//默认构造时后面不要加括号
    Person p1(10);
    Person p2(p1);
  • 显示法

    1
    2
    3
    Person p;
    Person p1=Person(10);
    Person P2=Person(p2);
  • 隐式转换法

    1
    2
    Person p4=10;
    Person p5=p4;

构造函数的调用顺序:

1.当普通构造一个对象时,程序先自动调用默认构造函数分配空间,在调用自定义构造函数(如果有的话)

2.当拷贝构造一个对象时,程序先自动调用默认构造函数分配空间,然后分两种情况,如果程序有自定义拷贝函数,则调用自定义拷贝构造函数;如果没有,就调用默认拷贝构造函数进行浅拷贝.

拷贝构造函数调用时机

c++拷贝构造函数调用时机

  • 使用一个创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值(做参数)
  • 以值的方式返回局部对象(做返回值)

构造函数调用规则

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

1.默认构造参数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性值进行拷贝

规则如下:

  • 如果用户定义有参构造函数,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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Person
{
public:
Person()
{
cout << "Person" << endl;
}
Person(int a,int height)
{
m_age =a;
m_height= new int(height);
}
Person(const Person &other)
{
cout << "Personkb" << endl;
m_age = other.m_age;
m_height =new int(*other.m_height);//深拷贝
}
~Person()
{
//析构,将堆区开辟的空间做释放操作
if(m_height !=NULL)
{
delete m_height;
}
cout << "Personxg" << endl;
}
int m_age;
int *m_height;
};

void test01()
{
Person p1(18,160);
cout << p1.m_age << endl;
cout << *p1.m_height << endl;
Person p2(p1);
cout << p2.m_age << endl;
cout << *p2.m_height <<endl;//会发生错误,因为p2是浅拷贝,但析构函数内部对p2进行了释放
}

int main()
{
test01();
return 0;
}

会出现:free():double free detected in tcache 2.

编译起提供的拷贝构造函数,只是简单的赋值,将原先的地址在复制一份。

所以需要深拷贝来解决该问题,自己实现拷贝构造函数

初始化列表

作用:c++提供了初始化列表语法,用来初始化(类的)属性

语法构造函数( ): 属性1(值1),属性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
class Person
{
public:
//Person(int a,int b,int c)
//{
//m_A=a;m_B=b;m_C=c;
//}
Person(int a,int b,int c):m_A(a),m_B(b),m_C(c)
{

}
int m_A;
int m_B;
int m_C;
}
void test
{
Person p;
}

int main()
{
test();
return 0;
}

初始化const成员变量的唯一方法就是使用初始化列表

类对象作为类成员

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

1
2
3
4
5
class A{}
class B
{
A a;
}//B类中有对象A作为成员,A为对象成员

那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?

先构造A对象.

当其他类的对象作为本类成员,构造时先构造类对象,在构造自身.

先析构B对象

析构时先析构自身,在析构类对象.

静态成员

静态成员就是在成员变量和成员函数前加上关键字static,成为静态成员

  • 静态成员变量

    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数

    • 所有对象共享一个函数
    • 静态成员函数只能访问静态成员变量,不可以访问非静态成员变量
      • 无法区分到底是哪一个对象的属性
      • 也是有访问权限的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
{
Public:
static int m_A;
}

int Person:: m_A=100;

void test()
{
Person P;
cout << P.m_A <<endl;//100
Person p2;
p2.m_A=200;
cout << P.m_A <<endl;//200
}

void test02()
{
cout <<Person :: m_A <<endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
public:
static void func()
{
cout << "static" << endl;
}
};

void test()
{
Person ::func();
}

int main()
{
test();
return 0;
}

c++对象模型和this指针

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

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

只有非静态成员变量才属于类的对象上

空对象的大小为1

c++编译器会给每一个空对象分配一个字节空间,是为了空对象占内存的位置.

每个空对象有一个独一无二的内存地址

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
31
32
33
34
35
class Person
{
public:
Person(int age)
{
//解决名称冲突
this->age = age;
}
Person& PersonAddage(Person &P)//要返回引用, 如果返回的是Person的值话,返回的是对p2的一份拷贝
{
this->age += P.age;
return *this;//返回*this
}
int age;
};

void test01()
{
Person p1(18);
cout << p1.age << endl;
}

void test02()//链式编程思想
{
Person p1(10);
Person p2(10);
p2.PersonAddage(p1).PersonAddage(p1).PersonAddage(p1);
cout << p2.age <<endl;
}

int main()
{
test02();
return 0;
}

空指针访问成员函数

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
23
24
25
26
27
28
class Person
{
public:
void showClassName()
{
cout << "Person" << endl;
}
void showPersonAge()
{
//报错原因是因为传入的指针是为NULL空的
if(this==NULL) return;
cout << "age=" << m_Age << endl;
}
int m_Age;
};

void test01()
{
Person *P=NULL;
P->showClassName();
// P->showPersonAge();
}

int main()
{
test01();
return 0;
}

const修饰成员函数

常函数:

  • 成员函数后加const后我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
    • 普通成员函数可以修改属性
1
2
3
4
5
6
//this指针的本质 是指针常量  指针的指向是不可以修改的
//const Person *const this
void showPerson()const
{
this->m_A =100;
}

在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改.

友元

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

友元的目的就是一个函数或者类 访问另一个类中私有成员

友元的关键字: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
    25
    26
    27
    class Building
    {
    //goodGay全局函数是 BUilding好朋友 ,可以访问Building中私有成员
    friend void goodGay(Building &building);
    public:
    Building()
    {
    m_SittingRoom ="客厅";
    m_BedRoom = "卧室";
    }
    public:
    string m_SittingRoom;
    private:
    string m_BedRoom;
    };

    void goodGay(Building &building)
    {
    cout << building.m_SittingRoom << endl;
    cout << building.m_BedRoom << endl;
    }

    void test01()
    {
    Building building;
    goodGay(building);
    }
  • 类做友元

    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 Building
    {
    //goodGay类是本类的好朋友,可以访问本类中的私有成员
    friend class goodGay;
    public:
    Building();
    public:
    string m_SittingRoom;
    private:
    string m_BedRoom;
    };


    class goodGay
    {
    public:
    goodGay();
    void visit();//参观函数访问Building中的属性
    Building *building;
    };

    goodGay::goodGay()
    {
    building = new Building;
    }

    void goodGay:: visit()
    {
    cout<<building->m_SittingRoom<<endl;
    cout<<building->m_BedRoom<<endl;
    }


    //类外去写成员函数
    Building::Building()
    {
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
    }

    void test01()
    {
    goodGay g;
    g.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
    44
    45
    46
    47
    48
    49
    class Building;

    class goodGay
    {
    public:
    goodGay();
    void visit1();//visit可以访问私有成员
    void visit2();//不可以访问私有成员
    Building *building;
    };

    class Building
    {
    friend void goodGay::visit1();
    public:
    Building();
    public:
    string m_SettingRoom;
    private:
    string m_BedRoom;
    };

    Building :: Building()
    {
    m_SettingRoom = "客厅";
    m_BedRoom = "卧室";
    }
    goodGay :: goodGay()
    {
    building = new Building;
    }

    void goodGay :: visit1()
    {
    cout << "Building " << building->m_SettingRoom <<endl;
    cout << "Building " << building->m_BedRoom <<endl;
    }

    void goodGay :: visit2()
    {
    cout << "Building " << building->m_SettingRoom <<endl;
    }

    void test()
    {
    goodGay g;
    g.visit1();
    g.visit2();
    }

运算符重载

元算符重载概念:对已有的运算符重新进行定义,赋予其另外一种功能,以适应不同的数据类型

加号运算符重载

作用:实现两个自定义数据类型相加的运算

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
Person p3=p1.operator+(p2);
简化为
Person p3=p1+p2;
class Person
{
public:
// Person operator+(Person &p)
// {
// Person temp;
// temp.m_A=this->m_A+p.m_A;
// temp.m_B=this->m_B+p.m_B;
// return temp;
// }
//本质上的调用是 Person p3=p1.operator+(p2);
int m_A;
int m_B;
};

Person operator+(Person &p1,Person &p2)//本质上调用是Person p3=operator+(p2,p1);
{
Person temp;
temp.m_A=p1.m_A+p2.m_A;
temp.m_B=p1.m_B+p2.m_B;
return temp;
}

void test()
{
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 10;
p2.m_B = 10;
Person p3=p1+p2;
cout << p3.m_A << endl;
cout << p3.m_B << endl;
}

运算符重载也可以发生函数重载.

总结:

  • 对于内置的数据类型的表达式的运算符是不可能改变的
  • 不要滥用运算符重载

左移运算符重载

可以输出自定义的数据类型

通常不会利用成员函数重载<<运算符,因为无法实现cout在左侧

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
{
public:
// 通常不会利用成员函数重载<<运算符,因为无法实现cout在左侧
// void operator<<()
// {
// }
int m_A;
int m_B;
};

//只能全局函数来重载左移运算符
ostream& operator<<(ostream &cout,Person &p)
{
cout << "m_A=" << p.m_A <<endl;
cout << "m_B=" << p.m_B <<endl;
return cout;
}

void test()
{
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p<<endl;
cout << p.m_A << " " << p.m_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
38
39
40
41
42
43
class MyIntegar
{
friend ostream& operator<<(ostream& os, MyIntegar s);
public:
MyIntegar()
{
m_Num = 0;
}
//重载前置++运算符
MyIntegar& operator++()
{
m_Num++;
return *this;
}
MyIntegar operator++(int)//int代表占位参数,可以用于区分前置和后置递增
{
MyIntegar temp=*this;
this->m_Num++;
return temp;
}
private:
int m_Num;
};

ostream& operator<<(ostream& os, MyIntegar s)
{
os << s.m_Num ;
return os;
}

void test()
{
MyIntegar myint;
cout << ++(++myint)<<endl;
cout << myint << endl;
}

void test2()
{
MyIntegar myint;
cout << myint++<<endl;
cout << myint << endl;
}

赋值运算符重载

c++编译器至少给一个类添加4个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

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
class Person
{
public:
Person(int age)
{
m_Age = new int(age);
}
Person& operator=(Person &p)
{
//应该先判断是否有属性在堆区,如果有先释放干净,然后在深拷贝
if(m_Age !=NULL)
{
delete m_Age;
}
m_Age= new int(*(p.m_Age));
return *this;
}
~Person()
{
if(m_Age!=NULL)
{
delete m_Age;
m_Age = NULL;
}
}
int *m_Age;
};

void test()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3=p2=p1;
cout << *p1.m_Age << endl;
cout << *p2.m_Age << endl;
cout << *p3.m_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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Person
{
public:
Person(string name,int age)
{
m_Name=name;
m_Age=age;
}
bool operator==(Person &P)
{
if(this->m_Name==P.m_Name && this->m_Age==P.m_Age)
{
return true;
}
return false;
}
bool operator!=(Person &P)
{
if(this->m_Name==P.m_Name && this->m_Age==P.m_Age)
{
return false;
}
return true;
}
string m_Name;
int m_Age;
};

void test()
{
Person p1("John",18);
Person p2("John",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
class Myprint
{
public:
//重载函数调用运算符
void operator()(string test)
{
cout << test << endl;
}
};

void test()
{
Myprint Myprint;
Myprint("helloworld");
}
1
2
3
4
5
6
7
8
9
void test02()
{
MyAdd myadd;
int ret=myadd(100,100);
cout << ret << endl;

//匿名函数对象
cout << MyAdd()(100,100)<< endl;
}

继承

继承是面向对象的三大特性之一

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

这个时候考虑利用继承技术,减少代码重复。

基本语法

​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
class BasePage
{
public:
void header()
{
cout << "首页、公开课、登陆、注册(公共头部)" << endl;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图、(公共底部)" << endl;
}
void left()
{
cout << "公共分类列表" << endl;
}
};

class Java: public BasePage
{
public:
void content()
{
cout << "java" << endl;
}
};

class Python : public BasePage
{
public:
void content()
{
cout << "phthon" << endl;
}
};

好处:减少重复代码。

继承方式

一共有三种:

  • 公共继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Base1
    {
    public:
    int m_A;
    protected:
    int m_B;
    private:
    int m_C;
    };

    class Son1 : public Base1
    {
    public:
    void func()
    {
    m_A = 10;
    m_B = 10;
    //m_C = 10;父类中的私有权限成员 子类访问不到
    }
    };
  • 保护继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //父类中的除私有权限下的成员之外的所有成员变为自己的保护权限下的成员
    class Base2
    {
    public:
    int m_A;
    protected:
    int m_B;
    private:
    int m_C;
    };

    class Son2 :protected Base2
    {
    public:
    void func()
    {
    m_A=10;
    m_B=10;
    }
    };
  • 私有继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //除私有成员之外的全部变为自己的私有成员
    class Base3
    {
    public:
    int m_A;
    protected:
    int m_B;
    private:
    int m_C;
    };

    class Son3 :private Base3
    {
    public:
    void func()
    {
    m_A=10;
    }
    };

继承中的对象模型

问题:从父类继承过来的成员,那些属于子类对象中?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};

class Son :public Base
{
public:
int m_D;
};

int main()
{
cout << sizeof(Son) << endl; //**16**
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
class Base
{
public:
Base()
{
cout << "Base" << endl;
}
~Base()
{
cout << "Basexg" << endl;
}
};

class Son :public Base
{
public:
Son()
{
cout << "Son" << endl;
}
~Son()
{
cout << "Sonxg" << endl;
}
};

void test()
{
Son s;
}

int main()
{
test();
return 0;
}

Base

Son

Sonxg

Basexg

继承中的构造和析构顺序:

  • 构造先构造父类,在构造子类
  • 析构先析构子类,在构造父类

继承中同名处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据?

  • 访问子类同名成员直接访问即可
  • 访问父类同名成员 需要加作用域
1
2
3
4
5
6
void test()
{
Son s;
cout << s.m_A << endl;
cout << s.Base::m_A << endl;//如果通过子类对象 访问到父类中同名成员,需要加作用域
}
1
2
3
4
5
6
7
//同名函数的处理方式
void test01()
{
Son s;
s.func();
s.Base::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
//同名静态成员属性
void test()
{
//通过对象访问
Son s;
cout << s.m_A << endl;
cout << Son:: m_A << endl;
cout << Base ::m_A << endl;
//通过对象访问
cout << s.Base::m_A << endl;
//第一个冒号 代表通过类名方式访问 第二个冒号代表父类作用域下
cout << Son::Base::m_A << endl;
}

//同名静态成员函数
void test01()
{
//通过对象访问
Son s;
s.func();
s.Base::func();
//通过类名访问
Son ::func();
Son::Base::func();
}

子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数

如果想访问父类中被隐藏同名成员,需要加作用域

多继承语法

C++允许一个类继承多个类

语法:class 子类 : 继承方式 父类1 ,继承方式 父类2 ……

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

1
class Son :public Base1,public Base2

总结:多继承中如果父类中出现了同名情况,子类使用时候要加作用域.

菱形继承

菱形继承概念:

​两个派生类继承同一个基类

​又有某个同类同时继承两个派生类

​这种继承被称为菱形继承,或者钻石继承

菱形继承典型案例

动物下有羊 与 驼继承了动物

底下又有草泥马(羊驼)同时继承了羊 与 驼

菱形继承问题:

1.羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性

2.草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以.

利用虚继承 解决菱形继承问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal
{
public:
int m_Age;
};
//利用虚继承 解决菱形继承问题
//继承之前加上virtual 变为虚继承
//Animal类称为 虚基类
//羊类
class Sheep : virtual public Animal{};
class Tuo : virtual public Animal{};
class SheepTuo :public Sheep,public Tuo{};

void test01()
{
SheepTuo st;
st.Sheep::m_Age = 18;
st.Tuo::m_Age = 28;
cout << st.Sheep::m_Age << endl;
cout << st.Tuo::m_Age << endl;
cout << st.m_Age << endl;
}

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

多态

多态是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
25
26
27
28
29
class Animal
{
public:
//虚函数
virtual void spaek()
{
cout << "speak Animal" << endl;
}
};

class Cat :public Animal
{
public:
void speak()
{
cout << "speak Cat" << endl;
}
};
//如果想执行对应的子类函数,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,晚绑定
void dospeak(Animal &A)
{
A.spaek();
}

void test()
{
Cat cat;
dospeak(cat);
}

原理深度剖析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal
{
public:
virtual void speak()
{
cout << "Animal Speak" << endl;
}
};

void test()
{
cout << sizeof(Animal) << endl;//8 这是一个指针的大小,当使用虚函数的时候,内部生成
//一个vfptr的指针,指向函数的所在位置
}

案例–计算器类

多态的优点:

  • 代码结构清晰
  • 可读性强
  • 立于前期和后期的拓展以及维护

纯虚函数和抽象类

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

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

语法:virtual 返回值类型 函数名 (参数列表) = 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
class Base
{
public:
//纯虚函数
//只要有一个纯虚函数,这个类称为抽象类
//特点:无法实例化对象 抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0 ;
};

class Son : public Base
{
public:
virtual void func()
{
cout << "实现函数" << endl;
}
};

void test()
{
//Base b;
//new Base;无法实例化对象
Son s;//子类必须重写纯虚函数
s.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
46
47
48
49
50
51
52
53
54
55
class AbstractDrinking
{
public:
virtual void Bool( ) = 0;
virtual void Brew( ) = 0;
virtual void PourInCup( ) = 0;
virtual void PutSomething( ) = 0;
void makeDrinking()
{
Bool( );
Brew( );
PourInCup();
PutSomething( );
}
};

class coffee :public AbstractDrinking
{
virtual void Bool( )
{
cout << "Coffeewater" << endl;
}
virtual void Brew( )
{
cout << "冲泡咖啡" <<endl;
}
virtual void PourInCup( )
{
cout << "倒入杯中" << endl;
}
virtual void PutSomething( )
{
cout << "Put something" << endl;
}
};

class Tea :public AbstractDrinking
{
virtual void Bool( )
{
cout << "Teawater" << endl;
}
virtual void Brew( )
{
cout << "茶叶咖啡" <<endl;
}
virtual void PourInCup( )
{
cout << "倒入杯中" << endl;
}
virtual void PutSomething( )
{
cout << "Put something" << 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
43
44
45
46
47
48
49
50
51
52
class Animal
{
public:
Animal()
{
cout << "Animal的构造函数" << endl;
}
// virtual ~Animal() //利用虚析构可以解决 父类指针释放子类对象时的不干净的问题
// {
// cout << "Animal的析构函数" << endl;
// }
//需要声明 ,也需要实现
virtual ~Animal () = 0;
virtual void speak()=0;
};

Animal :: ~Animal()
{
cout << "Animal的纯虚析构" << endl;
}

class Cat : public Animal
{
public:
Cat(string name)
{
cout << "cat的构造函数" << endl;
m_Name = new string(name);
}
virtual void speak()
{
cout << *m_Name << "Cat Speaking" << endl;
}
~Cat()
{
if(m_Name!=NULL)
{
cout << "cat的析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
string *m_Name;
};

void test01()
{
Animal *a = new Cat("Tom");
a->speak();
//父类指针在析构的时候 ,不会调用子类中的析构函数 ,导致子类如果有属性在堆区的话 ,出现了内存泄漏
delete a;
}
1
2
3
4
5
//虚析构语法:
virtual ~类名(){}
//纯虚析构语法:
virtual ~类名() = 0;
类民::~类名(){}

总结:

  • 虚析构或者纯虚析构就是用来解决通过父类指针释放子类对象
  • 如果子类中没有堆区数据,可以不写
  • 拥有纯虚析构函数的类也属于抽象类

案例3 – 组装电脑

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
class CPU
{
public:
virtual void calculate() = 0;
};

class VideoCard
{
public:
virtual void display() = 0;
};

class Memory
{
public:
virtual void storage() = 0;
};

class Computer
{
public:
Computer(CPU *cpu,VideoCard *vc ,Memory *mem)
{
m_mem = mem;
m_vc = vc;
m_cpu=cpu;
}
void work()
{
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
~Computer()
{
if( m_cpu!= NULL)
{
delete m_cpu;
m_cpu=NULL;
}
if(m_mem!=NULL)
{
delete m_mem;
m_mem=NULL;
}
if(m_vc!=NULL)
{
delete m_vc;
m_vc=NULL;
}
}
private:
CPU * m_cpu;
VideoCard *m_vc;
Memory *m_mem;
};


class Intelcpu:public CPU
{
virtual void calculate()
{
cout << "Intel CPU Start" << endl;
}
};

class IntelVideocard:public VideoCard
{
virtual void display()
{
cout << "Intel Videocard Start" << endl;
}
};

class IntelMemory:public Memory
{
virtual void storage()
{
cout << "Intel Memory Start" << endl;
}
};

class Lenovocpu:public CPU
{
virtual void calculate()
{
cout << "Lenovo CPU Start" << endl;
}
};

class LenovoVideocard:public VideoCard
{
virtual void display()
{
cout << "Lenovo Videocard Start" << endl;
}
};

class LenovoMemory:public Memory
{
virtual void storage()
{
cout << "Lenovo Memory Start" << endl;
}
};

void test()
{
CPU * intelcpu=new Intelcpu;
VideoCard * intelvideocard=new IntelVideocard;
Memory * intelmemory=new IntelMemory;
Computer * first= new Computer(intelcpu,intelvideocard,intelmemory);
first->work();
delete first;
cout << "-----------------------" << endl;
Computer * second= new Computer(new Lenovocpu,new LenovoVideocard,new LenovoMemory);
second->work();
delete second;
}

int main()
{
test();
return 0;
}

文件

通过文件可以将数据持久化

头文件

文件类型:

  • 文本文件 -文件以文本的Ascll码形式存储在计算机中
  • 二进制文件 -文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂

操作三大类:

  • ofstream 写操作
  • ifstream 读操作
  • fstream 读写操作

文本文件

打开方式解释
ios::in为读文件而打开文件
ios::out为写文件而打开文件
ios::app追加方式写文件
ios::trunc如果文件存在先删除,在创建
ios::binary二进制方式
ios::ate初始位置:文件尾

文件打开方式可以配合使用, 利用|操作符

总结:

  • 文件操作必须包含头文件fstream
  • 读文件可以利用ofstream 或者 fstream类
  • 打开文件时需要指定操作文件的路径,以及打开的方式
  • 利用 << 可以向文件中写数据
  • 操作完毕,关闭文件

读文件

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
void test()
{
ifstream ifs;
ifs.open("test.txt",ios::in);
if(!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
//接下来读取数据
//1.first
// char buf[1024]={0};
// while(ifs >> buf)
// {
// cout << buf << endl;
// }
//2.second
// char buf[1024]={0};
// while(ifs.getline(buf,1024))
// {
// cout << buf << endl;
// }
//3.third
// string buf;
// while(getline(ifs,buf))
// {
// cout << buf << endl;
// }
//4. fourth
char c;
while((c=ifs.get())!=EOF)
{
cout << c ;
}
ifs.close();
}

总结:

  • 读文件可以利用fstream ,或者fstream类
  • 利用is_open函数可以判断文件是否可以打开成功
  • close关闭文件

二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为ios::binary

写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型 : ofstream& write(const char *buffer ,int len)

参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
{
public:
char m_Name[64];
int m_Age;
};

void test()
{
// ofstream ofs;
// ofs.open("Person.txt",ios::out|ios::binary);
ofstream ofs("Person.txt",ios::out|ios::binary);
Person p = { "张三" , 18 };
ofs.write((const char *)&p,sizeof(Person));
ofs.close();
}

读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型: istream& read(char *buffer , int len);

参数解释:字符指针buffer指向内存中的一段存储空间,len是读写的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
{
public:
char m_Name[64];
int m_Age;
};

void test()
{
ifstream ifs;
ifs.open("Person.txt",ios::in | ios::binary);
if(!ifs.is_open())
{
cout << "打开失败" << endl;
}
Person p;
ifs.read((char *)&p,sizeof(Person));
cout << p.m_Name << endl;
cout << p.m_Age << endl;
ifs.close();
}

c++提高

  • 本阶段主要针对C++泛型编程和STL技术详解

模板

模板就是建立通用的模具,大大提高复用性

特点:

  • 模板不可以直接使用,他只是一个框架
  • 模板的通用并不是万能的

函数模板

  • c++另一种编程思想为泛型编程,主要利用的技术就是模板
  • c++提供两种模板机制:函数模板和类模板
1
2
template<typename T>
函数声明或定义(这里写函数)

解释:

  • template — 声明创建模板
  • typename —表明其后面的符号是一种数据类型,可以用class代替
  • T — 通用的数据类型 , 名称可以替换 , 通常为大写字母
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>//声明一个模板,告诉编译器后面代码中紧跟着的T不要报错,T是一个通用的数据类型
void myswap(T &a, T &b)
{
T temp=a;
a=b;
b=temp;
}

int main()
{
int a=10;
int b=20;
//两种方式使用函数模板
//1.自动类型推导
myswap(a,b);
//2.显示指定类型
myswap<int>(a,b);
return 0;
}

总结

  • 函数模板利用关键字template
  • 函数模板的两种使用方式:1.自动类型推导 2.显示指定类型
  • 模板的目的是为了提高复用性,将数据类型参数化

注意事项

  • 自动类型推导,必须要推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用
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
template <typename T>
void mySwap(T &a,T &b)
{
T temp=a;
a=b;
b=temp;
}

int main()
{
int a=10;
int b=20;
char c='c';
//正确的mySwap(a,b);
mySwap(a,c);//无法推导出一致的数据类型
return 0;
}

template <class T>
void func()
{
cout << "kjkasl"<<endl;
}

int main()
{
func();//没有指定T,函数内部都没有用到T
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
41
42
43
44
45
46
47
48
49
50
51
52
53
template <typename T>
int partition(T *a,int i,int j)
{
T x = a[i];
while(i<j)
{
while(i<j && a[j]>=x)
{
j--;
}
if(i<j)
{
a[i]=a[j];
i++;
}
while(i<j && a[i]<x)
{
i++;
}
if(i<j)
{
a[j]=a[i];
j--;
}
}
a[i] =x;
return i;
}

template <typename T>
void mySort(T *a,int i,int j)
{
if(i<j)
{
int x=partition(a,i,j);
mySort(a,i,x-1);
mySort(a,x+1,j);
}
}

void test()
{
char chararr[]= "bacdfe";
mySort(chararr,0,5);
cout << chararr << endl;
int num[]={4,5,1,2,9,7,0,3,6,8};
mySort(num,0,9);
for(int i=0;i<(sizeof(num)/sizeof(int));i++)
{
cout << num[i] << " ";
}
cout << endl;
}

普通函数与函数模板的区别

区别:

  • 普通函数调用时可以发生类型转换(隐式类型转换)
  • 函数模板调用时,如果是自动类型推导的话,不会发生类型转换
  • 如果显示指定类型的话,可以发生隐式类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T>
T myadd(T a, T b) {
return a + b;
}

int add(int a,int b)
{
return a + b;
}

void test()
{
int a=10;
int b=10;
char c='a';
add(a,c);
//myadd(a,c);
//显示指定类型
cout << myadd<int>(a,c) << endl;
}

总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T.

普通函数和函数模板的调用规则

调用规则:

  • 如果函数模板和普通函数都可以实现,优先调用普通函数
  • 可以通过空函数参数列表来强制调用函数模板
  • 函数模板也可以发生重载
  • 如果函数模板可以产生更好的匹配,优先调用函数模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename T>
void myprint(T a,T b)
{
cout << "函数模板" << endl;
}

void myprint(int a,int b)
{
cout << "普通函数" << endl;
}

void test()
{
int a = 10;
int b = 10;
myprint(a,b);//调用的是普通函数,规则1 ,将普普通函数注释,只有声明,会发生报错,由于都可以实现,先调普通函数,但普通函数没有实现
myprint<>(a,b);
}

总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性

模板的局限性

  • 模板的局限性并不是万能的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
void f(T a,T b)
{
a=b;
}
//如果传入的的是一个数组,就无法实现了.
template <typename T>
void f(T a,T b)
{
if(a>b)
{
......
}
}
//传入的是Person ,无法确定比较的数值

因此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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Person
{
public:
Person(string name, int age)
{
m_name = name;
m_age = age;
}
string m_name;
int m_age;
};

template <typename T>
bool mycompare(T &a, T &b)
{
if(a==b)
return true;
else
return false;
}

//利用具体化Person的版本实现代码,具体化优先调用
template<>bool mycompare(Person &a,Person &b)
{
if(a.m_name==b.m_name&&a.m_age==b.m_age)
{
return true;
}
else
return false;
}

void test()
{
int a=10;
int b=20;
cout << mycompare(a,b) << endl;//发生错误,无法比较。1.比较运算符重载 2.
}

void test01()
{
Person p1("Tom",10);
Person p2("Tom",10);
cout << mycompare(p1,p2)<<endl;
}

总结:

  • 利用具体化的模板,可以解决自定义类型的通用化
  • 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板

类模板

作用:建立一个通用的类,类中的成员 数据类型可以不具体指定,用一个虚拟的类型来代表

语法

1
2
template <typename T>

template —声明创建模板

typename —表明其后面是一种数据类型,可以用class代替

T —通用的数据类型,名称可以替换,通常为大写的数据类型

类模板和函数模板的语法相似,在template后面加类,此类称为类模板.

类模板与函数模板的区别

  • 类模板没有自动类型的推导的使用方式
  • 类模板在模板参数列表中可以有默认参数(仅限于类模板)
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
template <class T1,class T2=int>//模板参数列表
class Person
{
public:
Person(T1 name ,T2 age)
{
this->m_Name=name;
this->m_Age=age;
}
void show()
{
cout << this->m_Name << endl;
cout << this->m_Age << endl;
}
T1 m_Name;
T2 m_Age;
};

//类模板在模板列表中可以有默认参数
void test01()
{
Person<string >p1("Tom",17);
p1.show();
}

void test()
{
//Person p("John",18);,无类型推导的用法
Person<string ,int > p("John",18);
p.show();
}

总结:

  • 类模板使用只能显示指定类型方式
  • 类模板中的模板参数列表可以有默认参数

类模板中成员函数的创建时机

类模板中成员函数和普通类中成员函数创建时间是有区别的

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建
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
template <typename T>
class myClass
{
public:
T obj;
//类模板中的成员函数
void func1()
{
obj.showPerson1();
}
void func2()
{
obj.showPerson2();
}
};

class Person12
{
public:
void showPerson2()
{
cout << "Person2" << endl;
}
};

class Person1
{
public:
void showPerson1()
{
cout << "Person1" << endl;
}
};

总结:类模板中的成员函数并不是一开始就创建的,在调用时才去创建

类模板对象做函数参数

  • 类模板实例化的对象 ,向函数传参的方式

  • 三种传参方式

    • 指定传入的类型 —直接显示对象的类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      void print1(Person <string,int>&p)
      {
      p.showPerson();
      }

      void test01()
      {
      Person<string,int>p("孙悟空"100);
      }
    • 参数模板化 —将对象中的参数变为模板进行传递

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      template <class T1,class T2>
      void print2(Person <T1,T2>&p)
      {
      p.showPerson();
      cout << "T1的类型为: " << typeid(T1).name << endl;

      }

      void test02()
      {
      Person <string,int >p("tom",200);
      print2(p);
      }
    • 整个类模板化 —将这个对象类型 模板化进行传递

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      template <class T>
      void print3(T &p)
      {
      p.showPerson();
      }

      void test03()
      {
      Person<string ,int >p("唐僧",18);
      print3(p);
      }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <class T1 ,class T2>
class Person
{
public:
Person(T1 name ,T2 age)
{
this->m_Name=name;
this->m_Age=age;
}
void showPerson()
{
cout << this->m_Name <<endl;
cout << this->m_Age <<endl;
}

T1 m_Name;
T2 m_Age;
};

总结:

  • 通过类模板创建的对象,可以有三种方式向函数中进行传参
  • 使用比较广泛的是第一种:指定传入类型

类模板与继承

注意:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需要变为类模板
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
template <class T>
class Base
{
public:
T m;
};

//class Son : public Base //错误的,必须要知道父类中T的数据类型,才能继承给子类
class Son : public Base <int>
{

};

//想灵活指定类模板中的类型,子类也需要变为模板

template <class T1,class T2>
class Son1 : public Base<T2>
{
public:
Son1()
{
cout << typeid(T1).name << endl;
cout << typeid(T2).name << endl;
}
T1 obj;
};

void test()
{
Son s1;
}

void test01()
{
Son1 <int ,string> s;
}

如果父类是一个类模板,子类在继承的时候必须要指定出父类中T的数据类型。

类模板成员函数在类外实现

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
template <class T1,class T2>
class Person
{
public:
Person(T1 name ,T2 age);
// {
// this->m_Age=age;
// this->m_Name=name;
// }
void showName();
// {
// cout << this->m_Age << endl;
// cout << this->m_Name << endl;
// }
T1 m_Name;
T2 m_Age;
};

template <class T1,class T2>
Person<T1,T2> :: Person(T1 name ,T2 age)
{
this->m_Age=age;
this->m_Name=name;
}

template <class T1,class T2>
void Person<T1,T2> :: showName()
{
cout << this->m_Name << endl;
cout << this->m_Age << endl;
}

void test()
{
Person<string,int>p("John",18);
p.showName();
}

总结:类模板中的成员函数类外实现时,要加上模板参数列表.

类模板分文件编写

类模板成员函数创建实际是在调用阶段 ,导致分文件编写时链接不到

  • 直接包含.cpp源文件
  • 将声明和实现写到同一个文件当中,并更改后缀名为.hpp. .hpp是约定的名称,并不是强制的

​普通类中的成员函数一开始就可以创建

​类模板中的成员函数在调用时才创建

总结:主流的解决方式是第二种,将类模板和成员函数写到一起,并将后缀名改为.hpp

类模板与友元

全局函数实现 – 直接在类内声明友元即可

全局函数类外实现 – 提前让编译器知道全局函数的存在

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
template <class T1,class T2>
class Person;

//类外实现
template <class T1 ,class T2>
void printPerson2(Person<T1,T2> p)
{
cout << p.m_Name<<endl;
cout << p.m_Age<<endl;
}

//通过全局函数打印Person的信息
template <class T1 ,class T2>
class Person
{
//全局函数类内实现
friend void printPerson(Person<T1,T2> p)
{
cout << p.m_Name<<endl;
cout << p.m_Age<<endl;
}
//全局函数类外实现
//加空模板的参数列表
friend void printPerson2<>(Person<T1,T2> p);
public:
Person(T1 name ,T2 age)
{
this->m_Name=name;
this->m_Age=age;
}
private:
T1 m_Name;
T2 m_Age;
};

void test()
{
Person <string ,int> p ("Hello",18);
printPerson(p);
}

void test01()
{
Person <string ,int> p("ahjsd",19);
printPerson2(p);
}

总结:建议全局函数做类内实现,用法简单,而且编译器可以直接识别

总结: 字符串拼接的重载版本很多,记住几种即可.

myls

ls 是一个常用的命令行工具,用于列出指定目录中的文件和子目录。ls 的基本语法是:

1
ls [选项] [文件或目录]

以下是一些常用的 ls 命令选项:

  1. -l:以长格式显示文件信息,包括文件类型、权限、所有者、组、大小、修改时间等。

    1
    ls -l
  2. -a:显示所有文件,包括隐藏文件(以点开头的文件)。

    1
    ls -a
  3. -r:与 -l 逆序输出文件信息。

    1
    ls -r
  4. -R:递归显示子目录中的文件。

    1
    ls -R
  5. -t:按修改时间排序,最新修改的文件显示在前面。

    1
    ls -lt
  6. -S:按文件大小排序,最大的文件显示在前面。

    1
    ls -lS
  7. -i:显示文件的 inode 号码。

    1
    ls -i

思路

1.-i -a -R -r -i -s -l为命令行参数,我们首先需要解析命令行参数。

2.我们需要对以上参数进行了解,并进行构思一个框架以便后续对代码的维护与可读性。

3.我觉得可以将以上参数分为三类:1.-a 为确定需要显示的文件多少,例如-a包含 . 隐藏文件。2.-s -t 排序参数,如果含有该参数,则就需要对文件显示进行排序。3.-i -l 为输出参数,即在输出的时候进行判断是否需要输出对应的信息.

4.ls 还可以指定文件进行查看,我们还需要对不同的文件或目录进行操作.

相关函数

  • getopt,getcwd

  • opendir,readdir,closedir

  • snprintf

  • stat,lstat

主函数

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
enum order {
a, s, t, r, I, l, R
};//枚举命令行参数。提高代码的可读性.

int main(int argc, char *argv[])
{
int find = 0;//用来接收getopt的返回值
int orders[8] = {0};//存储对应参数的信息
for (int i = 1; i < argc; i++)
{
if (argv[i][0] == '-')
{
while ((find = getopt(argc, argv, "aIlrstR")) != -1)//解析命令行参数
{
switch (find)//将对应的参数信息残赋值为1.
{
case 'a':
orders[a] = 1;
break;
case 'r':
orders[r] = 1;
break;
case 's':
orders[s] = 1;
break;
case 't':
orders[t] = 1;
break;
case 'l':
orders[l] = 1;
break;
case 'R':
orders[R] = 1;
break;
case 'I':
orders[I] = 1;
break;
default:
fprintf(stderr, "Invalid option\n");
exit(EXIT_FAILURE);
}
}
}
}

int countfile = 0;
for (int i = 1; i < argc; i++)//获取文件的数量.
{
if (argv[i][0] != '-')
{
countfile++;
}
}

if (countfile == 0)
{
char current_path[MAX_PATH];//存储当前的工作路径
if ((getcwd(current_path, MAX_PATH)) != NULL)
{
if (orders[R] == 0)
{
do_open(current_path, orders);
}
else
{
do_open_R(current_path, orders);
}
}
else
{
perror("getcwd");
exit(EXIT_FAILURE);
}
}

for (int i = 1; i < argc; i++)
{
if (argv[i][0] != '-')
{
if (countfile > 1)
{
printf("%s\n", argv[i]);
}
if (orders[R] == 0)
{
do_open(argv[i], orders);
}
else
{
do_open_R(argv[i],orders);
}
}
}
}

了解了ls的基本语法与功能后,我们就需要进行实现.

那么就首先需要解析命令行参数.获取我们本次操作需要进行的工作.

getopt

该函数专门用来解析命令行参数.

1
int getopt(int argc, char * const argv[], const char *optstring);

参数说明

  • argc:通常由 main 函数直接传入,表示参数的数量

  • argv:通常也由 main 函数直接传入,表示参数的字符串变量数组

  • optstring:一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两

    个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号.

这里命令行参数已经解析,那么我们接下来就要进行对要查看的文件或目录数量进行统计.

如果数量大于2,就需要在进行操作前输出本次查看的文件或目录路径.

getcwd

1
2
#include<unistd.h>
char *getcwd(char *buf,size_t size);

介绍:
参数说明:getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buf的空间大小。

如果未传入文件或目录,就需要对当前路径下的文件进行操作.所以需要用到该函数.


现在已经获取到需要执行的参数和文件,接下来就需要进行参数操作.

所有参数中最难的就是递归部分,所以我们单独为-R封装一个函数来进行递归操作.

do_open

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
struct fileinfo//由于-l需要输出文件的详细信息,所以需要结构体来存储信息
{
unsigned int i_node;
char permission[16];
short owner;
short group;
off_t size;
time_t c_ctime;
time_t m_ctime;
nlink_t link_num;
char name[MAX_PATH];
mode_t mode;
blkcnt_t block;
};

void do_open(char *path, int orders[])
{
int count = 0;
struct fileinfo infos[MAX_FILE];//我们的-s -t -r参数需要对文件进行排序,那么我们就需要数组来存储文件相关信息。
if (orders[a])
{
ls_a(path, infos, &count);//ls-用来打开文件以及存储文件名称
}
if (orders[a] == 0)
{
only_ls(path, infos, &count);
}
if (orders[t])
{
qsort(infos, count, sizeof(struct fileinfo), cmp_time);//对存储的信息进行相关的排序.t的优先级高于s
}
if ((orders[s] == 1) && (orders[t] == 0))
{
qsort(infos, count, sizeof(struct fileinfo), cmp_size);
}
if ((orders[s] == 0) && orders[t] == 0)
{
qsort(infos, count, sizeof(struct fileinfo), cmp_name);//按名称排序,使文字命名文件在前
}
show_orders(infos, count, orders);//最后进行输出.
}

既然获取到了参数与文件,所以我们的do_open就需要接受这两类参数.

我们在do_open中进行文件打开,排序参数,以及展示.

count参数用于计数文件个数,便于输出结果.

do_open_R

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
void do_open_R(char *path, int orders[])
{
int count = 0;
struct fileinfo *infos = (struct fileinfo *)malloc(sizeof(struct fileinfo) * MAX_RFILE);
if (infos == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}
if (orders[a])
{
ls_a(path, infos, &count);
}
if (orders[a] == 0)
{
only_ls(path, infos, &count);
}
if (orders[t])
{
qsort(infos, count, sizeof(struct fileinfo), cmp_time);
}
if ((orders[s] == 1) && (orders[t] == 0))
{
qsort(infos, count, sizeof(struct fileinfo), cmp_size);
}
if ((orders[s] == 0) && orders[t] == 0)
{
qsort(infos, count, sizeof(struct fileinfo), cmp_name);
}
show_orders(infos, count, orders);
for (int i = 0; i < count; i++)
{
if (S_ISDIR(infos[i].mode) && strcmp(infos[i].name, ".") != 0 && strcmp(infos[i].name, "..") != 0)
{
char sub_path[MAX_PATH];
snprintf(sub_path, sizeof(sub_path), "%s/%s", path, infos[i].name);
struct stat flag;
if (lstat(sub_path, &flag) == -1)
{
fprintf(stderr, "Permission denied or error reading: %s\n", sub_path);
continue;
}
if(S_ISLNK(flag.st_mode))
{
continue;
}
do_open_R(sub_path,orders);
}
}
free(infos);
}

由于-R参数需要递归,所以采取堆上开辟空间进行存储

这里多了一个判断,如果存储的路径对应的仍为目录,则需要继续进行简单的do_open操作。

所以S_ISDIR用来判断是否是目录,如果是则进行递归操作。

S_ISLNK用来判断文件是否为符号链接,如果不加该判断,会在对 / 目录下/dev/fd中的符号链接进行叠加路径而出现错误.

ls_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
void ls_a(char *path, struct fileinfo *infos, int *count)
{
DIR *dir;//接收opendir的返回值.
struct dirent *dirp;//readdir的返回值
if ((dir = opendir(path)) == NULL)
{
printf("%s\n", path);
return ;
}
*count = 0;
struct stat st;
while ((dirp = readdir(dir)) != NULL)
{
char current_path[MAX_PATH];
snprintf(current_path, sizeof(current_path), "%s/%s", path, dirp->d_name);

if ((stat(current_path, &st)) != -1)//存储文件的相关信息。
{

strncpy(infos[*count].name, dirp->d_name, sizeof(infos[*count].name) - 1);
infos[*count].name[sizeof(infos[*count].name) - 1] = '\0'; // Null-terminate the string
infos[*count].group = st.st_gid;
infos[*count].owner = st.st_uid;
infos[*count].mode = st.st_mode;
infos[*count].c_ctime = st.st_ctime;
infos[*count].size = st.st_size;
infos[*count].m_ctime = st.st_mtime;
infos[*count].i_node = st.st_ino;
mode_to_letters(st.st_mode, infos[*count].permission);
infos[*count].link_num = st.st_nlink;
infos[*count].block=st.st_blocks;
}
else
{
perror("stat");
continue;
}
(*count)++;
}
closedir(dir);//一定要记得关闭文件
}

由于需要将信息进行存储,所以ls_a需要接收结构体数组,count 用于记录文件个数.

opendir

opendir()函数用于打开一个目录,并返回指向该目录的句柄,供后续操作使用。

1
2
3
#include <dirent.h>
DIR * opendir(const char * dirpath);
Returns directory stream handlc,or NULL on error

readdir

readdir()函数从一个目录流中读取连续的条目.

1
2
3
#include <dirent.h>
struct dirent *readdir(DIR * dirp);
Returns pointer to a statically allocated structure describing next directory entry,or NULL on end-of-directory or error

每调用 readdir()一次,就会从 dirp 所指代的目录流中读取下一目录条目,并返回一枚指针, 指向经静态分配而得的 dirent 类型结构,内含与该条目相关的如下信息: 每次调用 readdir()都会覆盖该结构。

closedir

closedir()函数用于关闭处于打开状态的目录,同时释放它所使用的资源.(一定要记得关闭文件,否则就会造成内存泄漏).

1
int closedir(DIR *dirp);

stat (获取文件信息)

1
2
3
4
5
#include <sys/types.h> 
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);

函数参数及返回值含义如下:

  • pathname:用于指定一个需要查看属性的文件路径。
  • buf:struct stat 类型指针,用于指向一个 struct stat 结构体变量。调用 stat 函数的时候需要传入一个 struct stat 变量的指针,获取到的文件属性信息就记录在 struct stat 结构体中。
  • 返回值:成功返回 0;失败返回-1,并设置 error。

struct stat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct stat 
{
dev_t st_dev; /* 文件所在设备的 ID */
ino_t st_ino; /* 文件对应 inode 节点编号 */
mode_t st_mode; /* 文件对应的模式 */
nlink_t st_nlink; /* 文件的链接数 */
uid_t st_uid; /* 文件所有者的用户 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 设备号(指针对设备文件) */
off_t st_size; /* 文件大小(以字节为单位) */
blksize_t st_blksize; /* 文件内容存储的块大小 */
blkcnt_t st_blocks; /* 文件内容所占块数 */
struct timespec st_atim; /* 文件最后被访问的时间 */
struct timespec st_mtim; /* 文件内容最后被修改的时间 */
struct timespec st_ctim; /* 文件状态最后被改变的时间 */
};

snprintf

snprintf 是一个 C 语言标准库函数,用于格式化字符串并将结果存储到一个字符数组中

1
int snprintf(char *str, size_t size, const char *format, ...);
  • tr: 指向存储结果的字符数组的指针。
  • size: 最大允许写入的字符数(包括字符串终止符)。
  • format: 格式化字符串,包含了要被写入到字符串中的文本和格式说明符。
  • ...: 可变数量的参数,用于替换格式字符串中的格式说明符。

snprintf 的行为类似于 printf,但是它会限制输出字符的数量,以防止溢出缓冲区。如果成功,snprintf 返回写入字符的总数(不包括字符串终止符),如果输出被截断,它会返回实际尝试写入的字符数。如果发生错误,返回负值。

完整代码

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <unistd.h>
#include <locale.h>
#include <ctype.h>
#include <sys/ioctl.h>
#include <stdbool.h>
#include <getopt.h>
#include <error.h>

#define MAX_PATH 1024
#define MAX_FILE 512
#define MAX_RFILE 60000

enum order {
a, s, t, r, I, l, R
};

struct fileinfo
{
unsigned int i_node;
char permission[16];
short owner;
short group;
off_t size;
time_t c_ctime;
time_t m_ctime;
nlink_t link_num;
char name[MAX_PATH];
mode_t mode;
blkcnt_t block;
};

void do_open(char *path, int orders[]);
void ls_a(char *path, struct fileinfo *infos, int *count);
void only_ls(char *path, struct fileinfo *infos, int *count);
void mode_to_letters(mode_t mode, char modestr[]);
int cmp_name(const void *a, const void *b);
int cmp_time(const void *a, const void *b);
int cmp_size(const void *a, const void *b);
void show_orders(struct fileinfo *infos, int count, int orders[]);
char *uid_to_name(uid_t uid);
char *gid_to_name(gid_t gid);
void do_open_R(char *path, int orders[]);
void print(struct fileinfo infos);

int main(int argc, char *argv[])
{
int find = 0;
int orders[8] = {0};
for (int i = 1; i < argc; i++)
{
if (argv[i][0] == '-')
{
while ((find = getopt(argc, argv, "aIlrstR")) != -1)
{
switch (find)
{
case 'a':
orders[a] = 1;
break;
case 'r':
orders[r] = 1;
break;
case 's':
orders[s] = 1;
break;
case 't':
orders[t] = 1;
break;
case 'l':
orders[l] = 1;
break;
case 'R':
orders[R] = 1;
break;
case 'I':
orders[I] = 1;
break;
default:
fprintf(stderr, "Invalid option\n");
exit(EXIT_FAILURE);
}
}
}
}

int countfile = 0;
for (int i = 1; i < argc; i++)
{
if (argv[i][0] != '-')
{
countfile++;
}
}

if (countfile == 0)
{
char current_path[MAX_PATH];
if ((getcwd(current_path, MAX_PATH)) != NULL)
{
if (orders[R] == 0)
{
do_open(current_path, orders);
}
else
{
do_open_R(current_path, orders);
}
}
else
{
perror("getcwd");
exit(EXIT_FAILURE);
}
}

for (int i = 1; i < argc; i++)
{
if (argv[i][0] != '-')
{
if (countfile > 1)
{
printf("%s\n", argv[i]);
}
if (orders[R] == 0)
{
do_open(argv[i], orders);
}
else
{
do_open_R(argv[i],orders);
}
}
}
}

void do_open(char *path, int orders[])
{
int count = 0;
struct fileinfo infos[MAX_FILE];
if (orders[a])
{
ls_a(path, infos, &count);
}
if (orders[a] == 0)
{
only_ls(path, infos, &count);
}
if (orders[t])
{
qsort(infos, count, sizeof(struct fileinfo), cmp_time);
}
if ((orders[s] == 1) && (orders[t] == 0))
{
qsort(infos, count, sizeof(struct fileinfo), cmp_size);
}
if ((orders[s] == 0) && orders[t] == 0)
{
qsort(infos, count, sizeof(struct fileinfo), cmp_name);
}
show_orders(infos, count, orders);
}

void do_open_R(char *path, int orders[])
{
int count = 0;
struct fileinfo *infos = (struct fileinfo *)malloc(sizeof(struct fileinfo) * MAX_RFILE);
if (infos == NULL)
{
perror("malloc");
exit(EXIT_FAILURE);
}
if (orders[a])
{
ls_a(path, infos, &count);
}
if (orders[a] == 0)
{
only_ls(path, infos, &count);
}
if (orders[t])
{
qsort(infos, count, sizeof(struct fileinfo), cmp_time);
}
if ((orders[s] == 1) && (orders[t] == 0))
{
qsort(infos, count, sizeof(struct fileinfo), cmp_size);
}
if ((orders[s] == 0) && orders[t] == 0)
{
qsort(infos, count, sizeof(struct fileinfo), cmp_name);
}
show_orders(infos, count, orders);
for (int i = 0; i < count; i++)
{
if (S_ISDIR(infos[i].mode) && strcmp(infos[i].name, ".") != 0 && strcmp(infos[i].name, "..") != 0)
{
char sub_path[MAX_PATH];
snprintf(sub_path, sizeof(sub_path), "%s/%s", path, infos[i].name);
struct stat flag;
if (lstat(sub_path, &flag) == -1)
{
fprintf(stderr, "Permission denied or error reading: %s\n", sub_path);
continue;
}
if(S_ISLNK(flag.st_mode))
{
continue;
}
do_open_R(sub_path,orders);
}
}
free(infos);
}


void ls_a(char *path, struct fileinfo *infos, int *count)
{
DIR *dir;
struct dirent *dirp;
if ((dir = opendir(path)) == NULL)
{
printf("%s\n", path);
return ;
}
*count = 0;
struct stat st;
while ((dirp = readdir(dir)) != NULL)
{
char current_path[MAX_PATH];
snprintf(current_path, sizeof(current_path), "%s/%s", path, dirp->d_name);

if ((stat(current_path, &st)) != -1)
{

strncpy(infos[*count].name, dirp->d_name, sizeof(infos[*count].name) - 1);
infos[*count].name[sizeof(infos[*count].name) - 1] = '\0'; // Null-terminate the string
infos[*count].group = st.st_gid;
infos[*count].owner = st.st_uid;
infos[*count].mode = st.st_mode;
infos[*count].c_ctime = st.st_ctime;
infos[*count].size = st.st_size;
infos[*count].m_ctime = st.st_mtime;
infos[*count].i_node = st.st_ino;
mode_to_letters(st.st_mode, infos[*count].permission);
infos[*count].link_num = st.st_nlink;
infos[*count].block=st.st_blocks;
}
else
{
perror("stat");
continue;
}
(*count)++;
}
closedir(dir);//一定要记得关闭文件
}


void only_ls(char *path, struct fileinfo *infos, int *count)
{
DIR *dir;
struct dirent *dirp;
if ((dir = opendir(path)) == NULL)
{
printf("%s\n", path);
return ;
}
*count = 0;
struct stat st;
while ((dirp = readdir(dir)) != NULL)
{
if (dirp->d_name[0] != '.')
{
char current_path[MAX_PATH];
snprintf(current_path, sizeof(current_path), "%s/%s", path, dirp->d_name);
if ((stat(current_path, &st)) != -1)
{
snprintf(infos[*count].name, sizeof(infos[*count].name), "%s", dirp->d_name);
infos[*count].group = st.st_gid;
infos[*count].owner = st.st_uid;
infos[*count].mode = st.st_mode;
infos[*count].c_ctime = st.st_ctime;
infos[*count].size = st.st_size;
infos[*count].m_ctime = st.st_mtime;
infos[*count].i_node = st.st_ino;
mode_to_letters(st.st_mode, infos[*count].permission);
infos[*count].link_num = st.st_nlink;
infos[*count].block = st.st_blocks;
}
(*count)++;
}
}
closedir(dir);//一定要记得及时释放
}

void mode_to_letters(mode_t mode, char modestr[]) {
strcpy(modestr, "----------");
if (S_ISDIR(mode)) modestr[0] = 'd';
if (S_ISCHR(mode)) modestr[0] = 'c';
if (S_ISBLK(mode)) modestr[0] = 'b';

if (mode & S_IRUSR) modestr[1] = 'r';
if (mode & S_IWUSR) modestr[2] = 'w';
if (mode & S_IXUSR) modestr[3] = 'x';

if (mode & S_IRGRP) modestr[4] = 'r';
if (mode & S_IWGRP) modestr[5] = 'w';
if (mode & S_IXGRP) modestr[6] = 'x';

if (mode & S_IROTH) modestr[7] = 'r';
if (mode & S_IWOTH) modestr[8] = 'w';
if (mode & S_IXOTH) modestr[9] = 'x';
}

int cmp_name(const void *a, const void *b)
{
const char *name_a = ((struct fileinfo *)a)->name;
const char *name_b = ((struct fileinfo *)b)->name;
setlocale(LC_COLLATE, "");
return strcoll(name_a, name_b);
}

int cmp_size(const void *a, const void *b)
{
off_t size_a = ((struct fileinfo *)a)->size;
off_t size_b = ((struct fileinfo *)b)->size;
return size_a > size_b ? (size_a < size_b ? -1 : 0) : 1;
}

int cmp_time(const void *a, const void *b)
{
time_t time_a = ((struct fileinfo *)a)->m_ctime;
time_t time_b = ((struct fileinfo *)b)->m_ctime;
return time_a > time_b ? (time_a < time_b ? -1 : 0) : 1;
}

void show_orders(struct fileinfo *infos, int count, int orders[])
{
long long total = 0;
if(orders[l])
{
for(int i = 0; i < count; i++)
{
total+=infos[i].block/2;
}
printf("总计 : %-11ld\n", total);
}
if (orders[r])
{
for (int i = count - 1; i >= 0; i--)
{
if (orders[I])
{
printf("%-8d", infos[i].i_node);
}
if(orders[s])
{
printf("%-8d", infos[i].block/2);
}
if (orders[l])
{
printf("%s ", infos[i].permission);
printf("%4d ", (int)infos[i].link_num);
printf("%-8s ", uid_to_name(infos[i].owner));
printf("%-8s ", gid_to_name(infos[i].group));
printf("%8ld ", infos[i].size);
printf("%.12s ", ctime(&infos[i].c_ctime) + 4);
}
print(infos[i]);
}
}
else
{
for (int i = 0; i < count; i++)
{
if (orders[I])
{
printf("%-8d", infos[i].i_node);
}
if(orders[s])
{
printf("%-8d", infos[i].block/2);
}
if (orders[l])
{
printf("%s ", infos[i].permission);
printf("%4d ", (int)infos[i].link_num);
printf("%-8s ", uid_to_name(infos[i].owner));
printf("%-8s ", gid_to_name(infos[i].group));
printf("%8ld ", infos[i].size);
printf("%.12s ", ctime(&infos[i].c_ctime) + 4);
}
print(infos[i]);
}
}
}

char *uid_to_name(uid_t uid)
{
struct passwd *pw_ptr;
pw_ptr = getpwuid(uid);
if (pw_ptr == NULL)
{
static char numstr[10];
snprintf(numstr, sizeof(numstr), "%d", uid);
return numstr;
}
else
{
return pw_ptr->pw_name;
}
}

char *gid_to_name(gid_t gid)
{
struct group *getgrgid(), *grp_ptr;
static char numstr[10];
if ((grp_ptr = getgrgid(gid)) == NULL)
{
sprintf(numstr, "%d", gid);
return numstr;
}
else
{
return grp_ptr->gr_name;
}
}

void print(struct fileinfo infos)
{
if (S_ISREG(infos.mode))
{
// Regular file
if (strstr(infos.name, ".c") != NULL)
{
// .c file, print in a different color
printf("\033[40;32m %s\033[0m\n", infos.name);
}
else if (strstr(infos.name, ".out") != NULL)
{
// .out file, print in another color
printf("\033[40;33m %s\033[0m\n", infos.name);
}
else
{
// Other regular file
printf("%s\n", infos.name);
}
}
else if (S_ISDIR(infos.mode))
{
// Directory
printf("\033[40;34m %s\033[0m\n", infos.name);
}
else
{
// Other file types
printf("%s\n", infos.name);
}
}


malloc&free

malloc(0)返回一个有效的空间长度为0的内存首地址,但是没法用(只能进行申请和释放).

动态申请数组指针

1
2
int (*P)[3]=(int(*)[3])=malloc(sizeof(int)*3);
int (*q)[2][3]=(int(*)[2][3])malloc(sizeof(int)*6);

初始化

malloc函数分配得到的内存是未初始化的.一般在使用该内存空间时,要调用memset来初始化为0.

1
void* memset(void *dest,int c,size_t count);

该函数可以将指定的内存空间按字节单位置为指定的字符c。其中,dest为要清零的内存空间的首地址,c为要设定的值,count为被操作的内存空间的字节长度。

1
void* memcpy(void* dest, void* src, size_t count);

此函数也是按照字节进行拷贝的,dest指向目标地址的指针,也就是要被赋值的空间首地址;src指向源地址的指针,也就是要被复制的空间的首地址;count跟memset()一样表示被拷贝的字节数;返回值也是被赋值的目标地址的指针。

其他申请方式

calloc

1
void* calloc(size_t num, size_t size); 

realloc

1
void* realloc(void* memblock, size_t size);  // 为已分配的内存空间重新申请内存块

_msize

1
size_t _msize(void* memblock);  // Windows平台下专用函数,非C语言标准函数

返回malloc() & calloc() & realloc()等申请内存块的大小,参数是分配内存块的首地址,也就是malloc() & calloc() & realloc()等的返回值。

free

malloc()申请一块内存空间,OS会有一张表记录所申请空间的首地址和这块地址的长度,free(空间首地址),free会从表中查找到这块首地址对应的内存大小,一并释放掉。

  1. free()不能去释放栈区的空间,栈区空间是由OS管理的,由OS进行申请和释放。
  2. 释放空间后,指针需要置空,避免成为野指针。
1
2
3
4
int* q = (int*)malloc(3);
free(q); // 会报错,int型指针一次操作 4Byte,这里只申请了 3Byte 相当去别人的地盘上拆东西,那肯定是不允许的
int* n = (int*)malloc(7); // 允许多申请,但是 int型指针一次只能操作 4Byte 多余的空间浪费了
free(n); // 释放时,从OS维护的表中查找到空间长度,会一并释放掉

new & delete

new

new 在申请基本类型空间时,主要会经历两个过程:

  1. 调用 operator new(size_t)operator new[] (size_t) 申请空间
  2. 进行强制类型转换
1
2
3
4
5
6
7
8
9
10
11
// ====== 申请单个空间 ======
type* p = new type;
// 执行上面这条语句实际的过程是下面的语句
void* tmp = operator new(sizeof(type)); // 调用 operator new(size_t) 申请空间
type* p = static_cast<type*>(tmp); // 进行强制类型转换

// ====== 申请数组空间 ======
type* q = new type[N];
// 执行上面这条语句实际的过程是下面的语句
void* tmp = operator new[](sizeof(type) * N); // 调用 operator new[](size_t) 申请空间
type* p = static_cast<type*>(tmp); // 进行强制类型转换

static_cast

是一种用于显示类型转换的强制转换运算符。是一个编译时运算符,用于在兼容类型之间执行转换.

1
2
3
static_cast<new_type>(expression)
int integerNumber = 42;
double doubleNumber = static_cast<double>(integerNumber);

new 在申请 object 空间时,主要会经历三个过程:

​1.调用 operator new(size_t)operator new[] (size_t) 申请空间

​2.进行强制类型转换

​3.调用类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// ====== 申请单个object ======
classname* p = new classname;
// 执行上面的语句实际的过程是下面的条语句
void* tmp = operator new(sizeof(classname)); // 调用 operator new(size_t) 申请空间
classname* p = static_cast<classname*>(tmp); // 进行强制类型转换
p->classname::classname(); // 调用类的构造函数,用户不允许这样调用构造函数,如果用户想调用可以通过 定位(placement)new 运算符 的方式调用

// ====== 申请object数组空间 ======
classname* q = new classname[N];
// 执行上面的语句实际的过程是下面的条语句
void* tmp = operator new[](sizeof(classname) * N + 4); // 调用 operator new[](size_t) 申请空间
classname* q = static_cast<classname*>(tmp + 4); // 进行强制类型转换
q->classname::classname(); // 调用 N次 构造函数

注意

申请的空间大小为

1
sizeof(classname)*N+4;

前4个字节写入数组大小,最后调用N次构造函数。

这里为什么要写入数组大小呢?释放内存之前会调用每个对象的析构函数。但是编译器并不知道 q 实际所指对象的大小。如果没有储存数组大小,编译器如何知道该把p所指的内存分为几次来调用析构函数呢?

new[] 调用的是operator new[],计算出数组总大小之后调用operator new。值得一提的是,可以通过()初始化数组为零值,实例:

1
char* p = new char[32]();

等同于:

1
2
char *p = new char[32];
memset(p, 32, 0);

new的底层

operator new(size_t) 是系统提供的全局函数,其 底层是由 malloc 实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试
执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)//是否注册了新的处理程序,没有则继续循环试图分配空间.
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}

delete

delete 的过程与 new 很相似,会调用 operator delete(void*)operator delete[] (void*) 释放内存。

1
2
3
4
5
6
7
delete p;
// 执行上面的代码实际过程是下面的语句
operator delete(p); // 调用 operator delete(void*); 释放空间

delete[] q;
// 执行上面的代码实际过程是下面的语句
operator delete[](q); // 调用 operator delete[](q); 释放空间

delete 释放 object 空间

  1. 调用类的析构函数
  2. 调用 operator delete(void*)operator delete[] (void*) 释放内存
1
2
3
4
5
6
7
8
9
delete obj;
// 执行上面的语句实际过程是下面的语句
obj->~classname(); // 首先调用类的析构函数
operator delete(obj); // 调用 operator delete(void*); 释放 object 内存空间

delete[] obj1;
// 执行上面的语句实际过程是下面的语句
obj->~classname(); // 调用 N次 类的析构函数
operator delete[](obj1); // 调用 operator delete[](void*); 释放 object 内存空间

new[]分配的内存只能由delete[]释放。如果由delete释放会崩溃,假设指针obj1指向new[]分配的内存,因为要4字节存储数组大小,实际分配的内存地址为obj1-4,系统记录的也是这个地址。delete[] 实际释放的就是obj1-4指向的内存。而delete会直接释放obj1指向的内存,这个内存根本没有被系统记录,所以会崩溃。

delete底层

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
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;

RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));

if (pUserData == NULL)
return;

_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);

/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));

_free_dbg( pUserData, pHead->nBlockUse );

__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY

return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

总结

new一个数组时,与malloc相似,os会维护一张记录数组头指针和数组长度的表.

释放基本数据类型的指针时,数组的头指针最终会被free(q)释放,所以调用delete q或者delete[] q,最终的结果都是调用free(q);

释放自定义类型的数组时,如果类中有需要在析构函数中释放,直接调用delete obj只会调用一次析构函数,后执行free(),就没有调用其他的析构函数,会造成内存泄漏.一定调用delete[] obj释放内存

new 和 delete的重写和重载

这两个函数在使用时,其实执行的是全局的::operator new::operator delete,如果我们在类中重载了这两个函数,那么就会调用我们实现的,没有则使用全局的. 对于基本数据类型则使用全局的.

1
2
3
4
5
6
7
8
9
10
//全局运算符定义格式
void* operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);

//类内运算符定义格式
class CA
{
void* operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);
};

标准库提供的全局的 operator new( 函数 ) 有六种重载形式,operator delete也有六种重载形式:

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
// =======================new=========================//
void *operator new(std::size_t count)
throw(std::bad_alloc); // 一般的版本
void *operator new[](std::size_t count)
throw(std::bad_alloc);

void *operator new(std::size_t count, // 兼容早版本的 new
const std::nothrow_t const&) throw(); // 内存分配失败不会抛出异常
void *operator new[](std::size_t count,
const std::nothrow_t const&) throw();

void *operator new(std::size_t count, void *ptr) throw(); // placement 版本
void *operator new[](std::size_t count, void *ptr) throw();

// ======================delete========================//
void * operator delete(void *) noexcept;
void * operator delete[](void *) noexcept;

void * operator delete(void *, std::nothrow_t const&) noexcept;
void * operator delete[](void *, std::nothrow_t const&) noexcept;

void * operator delete(void *, void *) noexcept;
void * operator delete[](void *, void *) noexcept;

// VS C++下还有两个,GCC下不确定
void * operator delete(void *, size_t) noexcept;
void * operator delete[](void *, size_t) noexcept;

new/delete运算符重载规则:

  1. new和delete运算符重载必须成对出现。
  2. new运算符的第一个参数必须是size_t类型的,也就是指定分配内存的size尺寸;delete运算符的第一个参数必须是要销毁释放的内存对象。其他参数可以任意定义。
  3. 系统默认实现了new/delete、new[]/delete[]、 placement new 5个运算符(expression)。它们都有特定的意义。使用它们在底层调用对应的函数,可以是系统提供的,也可能是被重写或重载的。
  4. 你可以重写默认实现的全局运算符,比如你想对内存的分配策略进行自定义管理或者你想监测堆内存的分配情况或者你想做堆内存的内存泄露监控等。但是你重写的全局运算符一定要满足默认的规则定义。也可以重载全局运算符,但也必须符合默认的规则,即第一个参数不能变。
  5. 如果你想对某个类的堆内存分配的对象做特殊处理,那么你可以重载这个类的new/delete运算符。当重载这两个运算符时虽然没有带static属性,但是不管如何对类的new/delete运算符的重载总是被认为是静态成员函数。
  6. 当delete运算符的参数>=2个时,就需要自己负责对象析构函数的调用,并且以运算符函数的形式来调用delete运算符。
  7. 这里的重载遵循作用域覆盖原则,即在里向外寻找operator new的重载时,只要找到operator new()函数就不再向外查找,如果参数符合则通过,如果参数不符合则报错,而不管全局是否还有相匹配的函数原型。比如如果这里只将Foo中operator new(size_t, const std::nothrow_t&)删除掉,就会在61行报错:

对象的自动删除

一般来说系统对new/delete的默认实现就能满足我们的需求,我们不需要再去重载这两个运算符。那为什么C++还提供对这两个运算符的重载支持呢?答案还是在运算符本身具有的缺陷所致。我们知道用new关键字来创建堆内存对象是分为了2步:1.是堆内存分配,2.是对象构造函数的调用。而这两步中的任何一步都有可能会产生异常。如果说是在第一步出现了问题导致内存分配失败则不会调用构造函数,这是没有问题的。如果说是在第二步构造函数执行过程中出现了异常而导致无法正常构造完成,那么就应该要将第一步中所分配的堆内存进行销毁。C++中规定如果一个对象无法完全构造那么这个对象将是一个无效对象,也不会调用析构函数。为了保证对象的完整性,当通过new分配的堆内存对象在构造函数执行过程中出现异常时就会停止构造函数的执行并且自动调用对应的delete运算符来对已经分配的堆内存执行销毁处理,这就是所谓的对象的自动删除技术。正是因为有了对象的自动删除技术才能解决对象构造不完整时会造成内存泄露的问题。

全局delete运算符函数所支持的对象的自动删除技术虽然能解决对象本身的内存泄露问题,但是却不能解决对象构造函数内部的数据成员的内存分配泄露问题,此时我们需要将析构函数中需要释放的数据成员的内存在重载的operator delete函数中进行释放。

operator new运用技巧

  • 用于调试
  • 内存池优化
  • STL中的new

new和malloc的区别

内存的申请所在位置

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

特别的,new甚至可以不为对象分配内存!定位new的功能可以办到这一点:

1
new(place_address) type

place_address为一个指针,代表一块内存的地址。

返回类型安全性

new返回对象类型的指针,类型严格匹配

而malloc返回void *,需要通过强制类型转换将void *转换成我们需要的类型.

内存失败时的返回值

new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

是否需要指定内存大小

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

是否调用构造函数/析构函数

使用new操作符来分配对象内存时会经历三个步骤:

  1. 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  2. 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
  3. 第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

  1. 第一步:调用对象的析构函数。
  2. 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

对数组的处理

C++提供了new[]与delete[]来专门处理数组类型,new[]分配的内存必须使用delete[]进行释放。

new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。

至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

1
int * ptr = (int *) malloc( sizeof(int)* 10 ); //分配一个10个int元素的数组

new与malloc是否可以相互调用

operator new 的实现可以基于malloc,而malloc的实现不可以去调用new。

是否可以被重载

opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本:

// 这些版本可能抛出异常

1
2
3
4
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete(void *) noexcept;
void * operator delete[](void *) noexcept;
1
2
3
4
5
// 这些版本承诺不抛出异常
void * operator new(size_t, nothrow_t const&) noexcept;
void * operator new[](size_t, nothrow_t const&) noexcept;
void * operator delete(void *, nothrow_t const&) noexcept;
void * operator delete[](void *, nothrow_t const&) noexcept
1
2
3
// VS C++下还有两个,GCC下不确定
void * operator delete(void *, size_t) noexcept;
void * operator delete[](void *, size_t) noexcept;

我们可以重载上面函数版本中的任意一个,前提是自定义版本必须位于全局作用域或者类作用域中。总之,我们有足够的自由去重载operator new /operator delete ,以决定我们的new与delete如何为对象分配内存,如何回收对象。

malloc/free并不允许重载。

能够直观地重新分配内存

使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

new没有这样直观的配套设施来扩充内存。

客户处理内存分配不足

在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new_handler。new_handler是一个指针类型:

1
2
3
4
namespace std
{
typedef void (*new_handler)();
}

指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:

1
2
3
4
namespace std
{
new_handler set_new_handler(new_handler p ) throw();
}

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。

对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

Saturday, 2 March 2024

As many have noticed, Neon’s release of Plasma 6 was not without its problems. We would like to apologise for the totally unexpected packaging problems as “Testing Edition” and “Unstable Edition” had been working just fine without these issues.

Of course the first round of fixes have already hit the “User Edition” archives. Expect more to follow as we continue to ‘Q.A.’ the packages and eliminate as many bugs as we can.

KDE got accepted as a mentoring organization for Google Summer of Code 2024! Are you thinking about getting involved into KDE development? Check out the cool ideas KDE devs came up, they showcase what can be achieved by taking part as a student in GSoC. How to start? How to get involved? How to make an impression that will help your application?

Google Summer of Code

Prerequisites

  • You like KDE, you like us as a community, you can follow our philosophy, you like our product (a desktop or at least an specific application), and you resonate with our tech stack (C++, Qt, CMake).
  • Grab some code from our GitLab, clone a repository and build it locally. This sounds easy. For first-timers it is not easy. Reach out for help in case you struggle.
  • Run your self-built software. Now you can explore the joy of developing KDE.

You do not need an idea, at least not yet. Give it some time.

Get involved

Try getting involved. Usually it is not easy to fix bugs of implement a feature request from KDE bugtracking system. Some are hard to fix. Others need debates of future directions or best ways to get things done.

I propose you start looking for other opportunities:

  1. Fix compiler warnings. Compilers analyze the code and as a result they might warn you. This can have various reasons like bad coding practice, code that is difficult to read and might easily be misread by humans, code with bad performance, bug-prone constructs.
  2. Fix deprecation warnings. KDE, Qt, and every software evolves. Old interfaces are replaced by newer ones. The old ones are not directly thrown away, but deprecated. The deprecation warning reminds the developer to migrate from the old to the new interface. Some deprecations are trivial to fix, others require ample code changes.
  3. Fix findings of static analyzers and linters. These are tools that analyze code more thorough compared to a compiler for the price of a longer runtime. They offer great hints for but are prone to false-positives (wrong warnings). Good tools for KDE are Cppcheck, Clazy and qmllint.
  4. Fix crash bugs. Crashes often occur when the code contains memory issues. Examples are using objects that were deleted, accessing arrays out of bounds, de-referencing null pointers. Tools like a good debugger, Valgrind, and AddressSanitizer (and its cousins MemorySanitizer and UndefinedBehaviorSanitizer) help to localize the problem. Crashes are more difficult to understand and fix compared to warnings for tools.

Try to work for fixes of one to ten cases. More makes reviewing harder. Create a pull request and wait for feedback.

Rationale

Why do I think these areas are good to start working? The maintainer might reject your pull request. This can always happen. Compared to implementing a whole new feature, the amount of work you invested is limited.

Once you have an idea for GSoC and write your application, you can point to your pull requests as proof of work. Maintainers see your involvement, they see how you interact, and you get an early sense whether you like the contributing experience or not.

Even in the case that you do not want to become an GSoC student, you improved KDE a tiny little bit. Great feeling, isn't it?

Word of warning

Not everybody deem warnings worth to be fixed -- in general or in specific cases. You will learn what kind of warnings getting fixed are welcome. I already wrote a blog post about my experiences with fixing some Cppcheck in Kile.

The Skrooge Team announces the release 2.32.0 version of its popular Personal Finances Manager based on KDE Frameworks.

Changelog

  • Correction bug 475294: Import file dialog show file://
  • Correction bug 475037: Dashboard: the font in "Income & Expenditure" is too small and has a bad contrast
  • Correction bug 478389: doc says pointed operations display a half-filled square, but it's 3 horizontal dots
  • Correction bug 477688: Problem with precision when doing internal transfer of bitcoin
  • Correction bug 480113: Skrooge: bookmarking a page adds a & to the bookmark name
  • Correction bug 481144: import tracker impossible, neither from homebank (TAG / ETIQUETTE)
  • Correction bug 481226: Improve compliance with messages HIG
  • Correction bug 483212: Can't add links in properties
  • Correction bug 482053: Difference in different Report-Selections

Thursday, 29 February 2024

It’s February already, and as expected I didn’t have too much time on my hands this month. I have some exciting related news though:

KDE Megarelease 6

If you somehow haven’t heard, a bunch of KDE 6-related stuff released yesterday! I highly recommend looking at the very nice announcement Carl and the rest of the KDE Promo team put together, it looks so good. Some of my changes in KCMs, Kiten, UnifiedPush support in Tokodon & NeoChat, PlasmaTube, and Tokodon are showcased there 🤩 One of my favorite changes is probably in KWin and the Wayland session, it’s near-perfect on my machine now.

NLnet Grant

For a while now, people have been asking about funding work on important Wayland features for artists and people who depend on accessibility features. We have been turning down offers from individual fundraisers, because behind the scenes my employer arranged a grant us to work on this task from NLnet! “Us” here means me and Nicolas Fella. I hope to begin in early April.

Tokodon

[Feature] I changed the thread visuals to improve the readability of replies. Note that it’s only one level deep, but even with that limitation I find it to be way better than it was before! [24.05]

Better thread visuals

[Feature] I did some neat notification changes, such as enabling group notifications by default and hiding the post actions unless required for notifications. The notifications page should have less visual noise now. [24.02.1]

Less visual noise on the notifications page

[Feature] Tokodon now warns if replies may be hidden from your server and suggests to open the original post in your web browser. This isn’t the greatest solution yet, eventually I want a way to load the post on the original server within Tokodon. This is a good start though, instead of showing nothing. [24.05]

The new replies message

NeoChat

[Bugfix] Rebased and cleaned up my bugfix to prevent editing messages destroying the formatting. This should be integrated soon!

Frameworks

[Bugfix] Fixed my typo that caused KFileMetadata to fail when not building with KArchive support, oops! [6.1]

[Bugfix] Add the missing and new Breeze icons dependency to kiconthemes. [6.1]

Wednesday, 28 February 2024

There are release parties around the world, the UK has a party in Cambridge.

Action plan is to go to The Haymakers, High Street, Cambridge and get pizza, drinks and more pizza. at 19:00 Thursday the 29th Feb.

Look for the guy in the blue KDE T-shirt.

Sign up at https://community.kde.org/Promo/Events/Parties/KDE_6th_Megarelease#Cambridge

Monday, 26 February 2024

Today, we bring you a report on the brand-new release of the Maui Project.

We are excited to announce the latest release of MauiKit version 3.1.0, our comprehensive user interface toolkit specifically designed for convergent interfaces, the complying frameworks, an in-house developed set of convergent applications, and the upcoming convergent shell environment for desktop and mobile devices.

Built on the foundations of Qt Quick Controls, QML, and the power and stability of C++, MauiKit empowers developers to create adaptable and seamless user interfaces across a range of devices, and with this release, we are a step closer to finalizing the migration to a new major version – the upcoming MauiKit4 release.

Join us on this journey as we unveil the potential of MauiKit3 for building convergent interfaces, the roadmap towards MauiKit4 and its new additions, and finally discover the possibilities offered by the enhanced Maui App stack.

Community

To follow the Maui Project’s development or to just say hi, you can join us on Telegram @mauiproject

We are present on Twitter and Mastodon:

Thanks to the KDE contributors who have helped to translate the Maui Apps and Frameworks!

Downloads & Sources

You can get the stable release packages [APKs, AppImage, TARs] directly from the KDE downloads server at https://download.kde.org/stable/maui/

All of the Maui repositories have the newly released branches and tags. You can get the sources right from the Maui group: https://invent.kde.org/maui

MauiKit4 & Documentation

MauiKit Calendar, Accounts, and Image Tools have now been ported to Qt6, joining MauiMan, MauiKit, and File Browsing frameworks. Alongside with the porting efforts, the frameworks are now documented, and available on the KDE API documentation site [HERE]. The remaining frameworks to be ported to Qt6 are MauiKit Terminal, Documents, and Text Editor, which should be fully ported and documented by the upcoming release in May.

The porting also involved updating the framework CMake code to the latest ECM changes introduced for creating QML plugins.

For the upcoming porting of MauiKit Text Editor, there are plans to move to a different backend for rendering the text more efficiently, and for Documents, the comics and ebooks backends will be reviewed and refactored to address crashing issues under Android.

You can find more information about the roadmap and plans for the migration to Qt6 at: https://invent.kde.org/maui/mauikit/-/issues/35

MauiKit Frameworks & Apps

A quick overview of the issues addressed and updates are covered in the following list:

  • Updated translations and support for more languages
  • Rebase MauiKit ApplicationWindow to QQC2 ApplicationWindow to resolve focus-stealing issues in Android
  • Update all the applications’ source code syntax in QML for the upcoming Qt6 port
  • Pick up the system light/dark preference for Android in MauiKit3, and move the handling of the Android status bar color from each app to MauiKit’s ApplicationWindow control. For MauiKit4 relay in the QStyleHints new properties
  • Split the  MauiApp code from the CSDControls. Thus register CSDControls as a new attached property: Maui.CSD [link to docs]
  • Expose the MauiKit Application root element via the attached property MauiApp, as Maui.App.rootComponent [link to docs]
  • Station fixes for handling the “terminate-session” shortcut and fixes to crashing issues when closing tabs, coming from MauiKit Terminal
  • The MauiKit’s PageLayout control has been backported from MauiKit4 to MauiKit3, and its implementation has gained new features, such as moving the collapsable elements to the footer. [link to docs]
  • Index app is now using MauiKit’s PageLayout control for splitting the toolbar actions for constrained spaces, the same as Pix
  • Pix fix the tags hot-reloading on new tags created
  • Fixes to nota syncing the terminal working directory
  • Vvave has gained a new option to quickly start a playlist in shuffle mode and a sleep timer: to stop playback and/or close the app after a certain time or end of the playlist
  • MauiKit’s AboutDialog has been revised and organized
  • MauiKit FileBrowsing control for creating a new file now picks the icon from the name extension
  • MauiKit Terminal and Text Editor now have a custom control exposing configurations for selecting custom color schemes
  • Index now has options to tweak the embedded terminal in Linux, such as custom color schemes, and reorganized setting entries for the terminal configurations
  • Nota now reuses the new Text Editor control for picking the color scheme and new options for  tweaking the embedded terminal
  • All of the apps now use an exported definition or the copyright notice
  • Fixes for all of the apps where the names of developers and other non-translatable strings were being marked as such
  • Fixed keyboard shortcuts for Station, Vvave, and other apps
  • Added style for the ComboBox component
  • MauiKit corrects the usage of singleton instances across threads for all the frameworks, which avoids the double instantiation from the CPP and QML side
  • Fixes to MauiKit TextEditor long press actions on touch screens
  • Fixes to style hints for the scrollbar policy coming from MauiMan
  • Fixes to Nota’s recent view selection mode
  • On mobile devices do not cache image or icon previews for the thumbnail delegates

[Known Bugs]

  • Nota crashes when opening a new file from the recent view under Android.
  • MauiKit Documents fails to open large comic books on Android, causing the app to crash due to the current design of using multiple threads for different pages. This is planned to be solved at MauiKitDocuments4 porting
  • MauiKit Text Editor TextArea has a flickering issue, which makes content jumpy on Android only. This is from upstream and should be solved on MauiKitTextEditor4 porting when moving to a different backend.
  • MauiKit FileBrowsing support for browsing SD cards on Android is currently disabled due to missing bindings to the “new” Android storage API

[What’s Next?]

For the next release, we plan to work on stabilizing the Maui Shell experience, by adding  XWayland support, and a few more things:

  • Finalize the porting of the MauiKit Frameworks to Qt6
  • Finalize and review the MauiKit documentation
  • Start the porting process of the Maui Apps to MauiKit4 AKA Qt6
  • Review MauiKit4 on Android
  • Review the migration of TextEditor to a new backend
  • Fixes to bugs on the Maui Apps stack
  • Update the Maui Apps features that are still pending

 

Maui Shell

For this release, Maui Shell and its components have received minimal updates coming from:

  • MauiCore and Maui Settings new modules for Audio and Network
  • Updated Maui Settings for MauiKit4 changes
  • Review shell aka Cask elements to be up to date with the MauiKit4 changes

 

That’s it for now. Until the next blog post, that will be a bit closer to the 3.1.1 stable release.

To follow the Maui Project’s development or say hi, you can join us on Telegram: https://t.me/mauiproject.

We are present on Twitter and Mastodon:

New release schedule

The post Maui Release Briefing #5 appeared first on MauiKit — #UIFramework.

Sunday, 25 February 2024

You may have already read about it on Volkers blog: we together with people from other public transport related projects are building a public transport routing service called Transitous. While of course our main motivation is to use it in KDE Itinerary, KDE’s travel planning app, it will be open for use in other apps.

We also have a little web interface running at transitous.org.

We are building this service based on great existing software, in particularly MOTIS.

Screenshot of the Transitous web interface, showing the positions of long-distance transit vehicles in Germany, the Netherlands, Switzerland, Latvia, Estonia and Sweden

Now, to make this really useful, we need data on more regions. Luckily, for most regions and countries that is fairly easy. Most transport agencies and countries make GTFS feeds available, that we can just use.

Adding an additional feed doesn’t take long and doesn’t need programming experience. It’s pretty much just creating a small text file that explains how and where to download the data from.

Those links don’t necessarily stay the same forever, so we would be happy if lots of people take care of their region, and update the link every few years. It is really little work if split up, but can’t all be handled by a small team.

To make it even easier, we can already use the Transitland Atlas feed collection, for which you just need to choose the feed to use. The url will then automatically be looked up.

You can find out how to add a feed here. Please let us know if the documentation is unclear anywhere.

If you are interested in using this service in your own application, it is probably a bit too early for production, but it makes sense to already implement support for the MOTIS API that we use. You can find an early version of our API documentation here.

If there is anything else you are interested in helping with, for example improving our ansible playbook, creating a website, improving MOTIS or working on integrating OpenStreetMap routing, you can find our open tasks here. We appreciate any help on those issues, and it of course speeds up the development of the project.