CPP基础知识补充
CPP基础知识补充
前言:
为了准备RM招新面试,先复习一下菜的不能再菜的CPP基础。
2024.11
1.按位运算
①按位或(OR)
只要两个相应的位中至少有一个为1时,结果位就为1。主要用于设置指定的位。
//用法如下
a | b
例如,对二进制的1101和1011进行按位或,就会得到1111。
1101 (13)
1011 (11)
----
1111 (15)
②按位与(AND)
只有当两个相应的位都为1时,结果位才为1。主要用于屏蔽位或清除指定的位。
//用法如下
a & b
同样对上面的例子进行按位与,就会得到1001。
1101 (13)
1011 (11)
----
1001 (9)
③按位异或(XOR)
当两个相应的位相异时,结果位为1。主要用于切换指定的位。
//用法如下
a ^ b //这不是次方
1101 (13)
1011 (11)
----
0110 (6)
④按位取反(NOT)
对操作数的每一位进行取反操作,即将1变为0,将0变为1。主要用于生成掩码和反转位的值。
//用法如下
~a;
1101 (13)
----
0010 (2 的二进制补码表示,如果是8位表示则为 11110010)
⑤左移和右移
**左移 <<:
- 用法:
a << n - 将a的二进制表示向左移动n位,右侧空出的位用0填充。主要用于将数值乘以2的幂。
示例:
0011 (3) << 2
----
1100 (12)
右移 >>:
- 用法:
a >> n - 将a的二进制表示向右移动n位,对于无符号类型,左侧空出的位用0填充;对于有符号类型,通常左侧会用符号位填充(取决于实现)。
示例:
1101 (13) >> 2
----
0011 (3)
2.成员运算符
1. 点运算符(.)
- 用法:用于直接通过对象来访问其成员。
- 适用情况:当你拥有一个对象时,可以直接使用
.运算符来访问对象的公有成员(属性或方法)。
示例:
class Person {
public:
string name;
void display() {
cout << "Name: " << name << endl;
}
};
int main() {
Person person;
person.name = "Alice";
person.display(); // 使用点运算符
}
在这个例子中,person是一个Person类的对象,我们通过.运算符访问其name属性和display()方法。
2. 箭头运算符(->)
- 用法:用于通过指针来访问其所指向的对象的成员。
- 适用情况:当你拥有一个指向对象的指针而非对象本身时,可以使用
->运算符来访问指向对象的公有成员。
示例:
class Person {
public:
string name;
void display() {
cout << "Name: " << name << endl;
}
};
int main() {
Person *person = new Person();
person->name = "Bob";
person->display(); // 使用箭头运算符
delete person;
}
在这个例子中,person是一个指向Person类对象的指针。我们通过->运算符来访问其name属性和display()方法。
总的来说,如果你有一个对象实例,你会使用.运算符;如果你有一个指向对象的指针,你会使用->运算符。这两种运算符提供了类似的功能,但它们的使用取决于你是直接操作对象还是操作对象的指针。
3.CPP中的类型转换
1. C风格转换
这是从C语言继承来的传统转换方法,使用括号包围的目标类型来进行转换。例如:
double x = 9.5;
int y = (int)x; // C-style cast
尽管简单,但C风格转换不具类型安全,可能导致无意的数据丢失或错误,因此通常不推荐使用。
2. 函数风格转换
与C风格类似,只是看起来像是函数调用。例如:
double x = 9.5;
int y = int(x); // Function-style cast
这种方式也有同样的缺点,功能与C风格转换相同,同样不安全。
3. 静态转换(static_cast)
static_cast是用于类型明确无误且不需要运行时检查的情况。且转换过程中不需要类型检查的情况。这种转换在编译时就确定了,比C风格转换安全。
double x = 9.5;
int y = static_cast<int>(x); // Static cast
static_cast在这里也是用来去除小数部分,但它比C风格转换更加明确和安全。
4. 动态转换(dynamic_cast)
dynamic_cast主要用于处理类的继承关系,特别是在涉及多态时。它在运行时检查类型是否可以安全转换,如果不可以,则转换失败。
class Base {};
class Derived : public Base {};
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 尝试将base转换为Derived类型
如果base实际指向Derived类型的对象,转换就会成功,否则结果为nullptr。
5. 重新解释转换(reinterpret_cast)
这种转换用于进行低级别的转换,如指针类型之间的转换。它只是简单地重新解释内存中的位模式,而不改变位模式本身。
int* p = new int(10);
char* ch = reinterpret_cast<char*>(p); // Reinterpret cast
这里,int指针被转换为char指针,允许按字符访问int值的内存。
6. 常量转换(const_cast)
const_cast用于添加或删除对象的const属性。这通常用于与不接受const参数的旧API接口时必须去除const限定。例如:
const int x = 10;
int* y = const_cast<int*>(&x); // Const cast
使用const_cast时需要特别小心,因为修改一个本应是const的值可能导致未定义行为。
4.CPP中的结构体
在C++中,结构体(struct)是一种用户定义的数据类型,可以用来组合不同或相同类型的数据项。结构体在C和C++中都有定义,但在C++中它们拥有更丰富的功能。同时,结构体与C++中的类(class)非常相似,但有一些关键的区别。
C中的结构体
在C语言中,结构体主要用于数据打包,可以包含多个不同类型的数据项,但不能包含函数或方法。C的结构体是一种将相关数据组织在一起的简单工具,主要用于表示数据记录。
示例:
struct Person {
char name[50];
int age;
float salary;
};
C++中的结构体
C++扩展了结构体的定义,使其不仅能够包含数据,还能包含函数。在C++中,结构体几乎和类一样强大,包括能够有成员函数、构造函数、析构函数、继承等。
示例:
struct Person {
char name[50];
int age;
float salary;
void print() {
std::cout << "Name: " << name << ", Age: " << age << ", Salary: " << salary << std::endl;
}
};
CPP中结构体与类的区别
虽然在C++中结构体和类非常相似,但它们之间存在几个关键的区别:
- 默认的访问控制:
- 在
struct中,默认的访问级别是**public**。这意味着,除非你明确指定,否则结构体的成员和继承都是公开的。 - 在
class中,默认的访问级别是private。这意味着,如果不进行显式声明,类的成员和继承都是私有的。
- 在
- 用途:
struct通常用于较小的数据结构,有少量数据和少数简单的操作,其主要目的是数据存储。class则用于定义更复杂的对象,通常包括多个数据项和操作这些数据的方法,更适合用于实现抽象和封装。
C结构体与C++结构体的区别
- 成员函数:
- C结构体不能包含成员函数或构造函数等。
- C++结构体可以包含成员函数,构造函数,析构函数等。
- 继承和多态:
- C结构体不支持继承和多态。
- C++结构体支持继承和多态,可以有基类和派生类。
- 访问修饰符:
- C结构体不具有访问修饰符。
- C++结构体支持
public、private和protected等访问修饰符。
总结来说,C++中的结构体是C中结构体概念的扩展,提供了更多面向对象的特性,包括方法、继承等。同时,C++的结构体和类非常类似,主要区别在于默认的访问权限和一般的用途上。在设计C++程序时,根据实际需要选择使用结构体或类是很重要的。
5.内存对齐
为什么需要内存对齐
计算机的内存是按字节组织的,但是计算机的CPU访问内存不总是一次一个字节。通常,CPU一次可以读取特定数量的字节,这个数量称为“字”(word),常见的如4字节或8字节。为了最大限度地提高内存访问的效率,CPU访问内存时更喜欢从某个对齐的地址开始读取,这个对齐的地址通常是它的自然界限(如4字节或8字节的倍数)。
如果数据未按其自然界限对齐,CPU可能需要进行多次内存访问才能读取完整的数据,这会减慢程序的运行速度。因此,内存对齐可以帮助提升性能,尤其是在访问结构体成员、数组元素等复合数据结构时更为重要。
内存对齐的规则取决于编译器和硬件的特定要求,但一般遵循以下原则:
- 变量的对齐界限
- 基本数据类型(如
int、float等)应该对齐到其大小的界限。例如,如果int是4字节,它应该存储在4字节对齐的地址上。
- 基本数据类型(如
- 结构体的对齐
- 结构体的总对齐要求通常是其最大成员的对齐要求。
- 结构体内每个成员相对于结构体开始的位置也应符合该成员类型的对齐要求。
- 结构体的总大小也应该是其最大对齐要求的倍数,这样在结构体数组中每个元素都能正确对齐。
结构体内存对齐
假设有以下结构体:
struct Example {
char a; // 字节 1
int b; // 4 字节
char c; // 字节 1
};
按照内存对齐的规则:
char类型通常对齐到1字节。int类型通常对齐到4字节。
因此,为了使int b对齐,char a后面会有一些填充字节。整个结构可能会被编译器自动调整为:
struct Example {
char a; // 1字节
char padding1[3]; // 3字节填充,确保`int b`从4字节界限开始
int b; // 4字节
char c; // 1字节
char padding2[3]; // 3字节填充,确保结构体大小是最大对齐界限(4字节)的倍数
};
总大小变为12字节,确保每个结构体成员都满足其对齐要求。
尽管我们在代码中没有明确写出char padding1和char padding2,编译器会自动添加这些填充字节来确保每个成员变量满足内存对齐的要求。这个过程是自动的,目的是为了优化数据访问的性能。
6.new和delete
在解释new和delete等动态内存管理操作之前,了解堆和栈的区别能帮助更好地理解这些操作的背景和必要性。
- 栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量、函数参数和函数调用后 返回的地址。(为运行函数而分配的局部变量、函数参数、函数调用后返回地址等存放在栈 区)。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限,过多的使 用会造成程序崩溃,通常发生在函数递归过深。
- 堆:内存使用
new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的 释放,会造成内存泄漏。但现在的计算机都有内存保护机制,在程序结束时,会自动回收。
new 运算符
new 运算符用于在堆上分配内存。它会返回指向新分配的内存的指针。使用 new 时,还会自动调用对象的构造函数(如果有的话),这是它与 C 语言中的 malloc 函数的一个重要区别。
基本语法:
TypeName* pointer = new TypeName;
如果 TypeName 是一个类类型,那么它的构造函数会被调用。
示例:
int* ptr = new int; // 分配一个整数
*ptr = 5; // 存储值 5
也可以在 new 表达式中初始化对象:
int* ptr = new int(5); // 分配一个整数,并初始化为 5
对于数组:
int* array = new int[10]; // 分配一个有 10 个整数的数组
delete 运算符
与 new 相对应,delete 运算符用于释放先前通过 new 运算符分配的内存,并调用相应对象的析构函数(如果适用)。正确配对 new 和 delete 的使用是非常重要的,以防止内存泄露。
基本语法:
delete pointer;
对于通过 new 分配的数组,应使用 delete[] 运算符:
delete[] array;
示例:
delete ptr; // 释放之前分配的整数
delete[] array; // 释放整个数组
但是
-
匹配使用:对每个
new应有相应的delete,对每个new[]应有相应的delete[]。不匹配使用可能会导致运行时错误。 -
避免重复删除:删除已经被
delete的内存会导致未定义行为(通常是程序崩溃)。确保指针在delete后设为nullptr可以帮助避免这种情况。delete ptr; ptr = nullptr; -
内存泄漏:如果忘记释放通过
new分配的内存,会导致内存泄漏。这在长时间运行或资源有限的程序中尤为严重。 -
初始化:使用
new分配的基本数据类型的内存不会自动初始化。未初始化的内存可能含有任意值。 -
异常处理:如果
new无法分配足够的内存,它将抛出一个std::bad_alloc类型的异常,而不是返回nullptr(像 C 的malloc那样)。这应该在代码中得到适当处理。
7.引用与指针
在CPP中,引用和指针一样都可以用来访问其他变量的内存地址,但二者在用法和底层实现上有一些关键区别。
引用(References)
引用本质上是变量的别名。一旦一个引用被初始化为指向一个变量后,它就一直指向那个变量;你不能改变它指向的变量。这是引用的一个核心特征:它们必须在定义时被初始化,并且不能被重新指向另一个变量。
特性:
- 引用在内部实现时可能使用指针,但作为一种抽象,它就像是目标变量的一个完全同步的别名。
- 引用不能为
nullptr,总是必须引用某个具体的变量。 - 引用在使用时像普通变量一样,不需要解引用操作。
示例:
int x = 10;
int& ref = x; // ref是x的引用
ref = 20; // 实际上是改变了x的值
std::cout << x << std::endl; // 输出20,因为ref就是x的另一个名字
指针(Pointers)
指针是存储另一个变量内存地址的变量。与引用不同,指针可以在其生命周期内被重新指向不同的变量,也可以指向 nullptr 表示不指向任何对象。
特性:
- 指针需要通过解引用操作符(
*)来访问目标变量的值。 - 指针可以指向
nullptr,表示它不指向任何变量。 - 指针的值可以改变,可以指向另一个变量或者指向动态分配的内存区域。
示例:
int x = 10;
int* ptr = &x; // ptr是指向x的指针
*ptr = 20; // 改变ptr指向的内存的值(即x的值)
std::cout << x << std::endl; // 输出20
ptr = nullptr; // ptr现在不指向任何内存
引用与指针的区别
- 初始化和可变性:
- 引用必须在创建时被初始化且不能改变目标,这使得引用在使用上更安全、更直接。
- 指针可以在定义后改变指向,提供了更大的灵活性但也增加了错误发生的机会(如野指针和内存泄漏问题)。
- 语法和访问:
- 引用的语法更简洁,访问引用的变量就像访问普通变量一样。
- 指针需要显式解引用来访问目标变量,语法更为复杂。
- 用途:
- 引用常用于函数参数和返回值,使得函数调用更简洁且避免拷贝(尤其是对于大对象的处理)。
- 指针适用于更复杂的任务,如动态内存管理、数据结构(如链表和树)中的节点指向等。
- 不存在空引用,引用必须连接到一块合法的内存。
总结来说,引用提供了一种更安全、更易于使用的方法来访问其他变量,而指针提供了更高的灵活性和控制能力。根据具体的需要和场景,选择使用引用或指针是 C++ 编程中的一个重要考虑。
引用传参
引用传参(pass by reference)是一种将函数参数传递给函数的方法,它允许函数直接操作实参而非其副本。通过引用传参,函数中对参数的任何修改都会直接反映在原始数据上。这种方式在处理大型数据结构或进行资源管理时尤为有用,因为它避免了不必要的数据复制,从而提高了效率。
void increment(int& num) {
num += 1; // 直接修改传入的参数
}
int main() {
int value = 5;
increment(value); // 传递引用
std::cout << "Value: " << value << std::endl; // 输出6,显示value被修改
}
在这个例子中,increment 函数通过引用接收num参数,因此在函数内部对num的修改直接影响了main中的value变量。
8.面向对象与面向过程
面向对象(Object-Oriented Programming, OOP)和面向过程(Procedural Programming)是两种常见的编程范式,它们有着根本的区别,影响着程序的结构、设计和实现方式。理解这两种范式的核心概念和区别对于选择适合特定问题的编程方法非常重要。
面向过程编程
面向过程编程是一种基于功能和过程的编程范式。在这种范式中,程序被视为一系列的程序调用或过程(也称为函数或子程序),它们执行数据上的操作。面向过程编程强调的是函数的顺序执行,适用于解决简单任务和直线型问题,其核心在于操作数据和实现功能。
特点:
- 结构化:程序被分解成可重用的函数或过程。
- 功能导向:每个函数都专注于执行一个具体的任务。
- 数据与函数分离:数据结构和操作数据的函数相互独立。
示例:在C语言中,我们可以定义数据结构和函数来操作这些数据,但它们在概念上是分开的。
struct Student {
char name[50];
int age;
};
void print_student(struct Student s) {
printf("Name: %s, Age: %d\n", s.name, s.age);
}
面向对象编程
面向对象编程是一种将现实世界的事物模拟为对象的编程范式。每个对象可以包含数据(称为属性)和操作数据的函数(称为方法)。面向对象编程的核心在于对象的概念,通过封装、继承和多态来实现代码的重用、灵活性和易维护性。
特点:
- 封装:隐藏对象的内部状态和实现细节,仅通过对象的方法进行交互。
- 继承:允许新创建的类(子类)继承现有类(父类)的属性和方法。
- 多态:允许不同类的对象对同一消息作出响应,具体行为取决于对象的实际类型。
示例:在C++中,我们可以定义一个类来封装数据和操作数据的方法。
class Student {
public:
char name[50];
int age;
void print() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
在这个例子中,Student 类不仅包含数据(如 name 和 age),还包含一个方法 print() 来操作这些数据。
区别总结
- 设计焦点:面向过程编程关注于过程(函数),面向对象编程关注于对象。
- 代码组织:面向过程将程序分解为函数,而面向对象将程序分解为对象。
- 代码重用:面向对象通过继承和多态机制更容易实现代码的重用。
9.对象和类
类是一个蓝图或模板,用于创建具体的对象。它定义了一组属性(成员变量)和方法(成员函数),这些属性和方法共同描述了一种特定类型的对象的行为和状态。
- 属性:属性是类中定义的变量,用于存储数据。属性表示对象的状态或特征。
- 方法:方法是类中定义的函数,用于执行操作。方法可以访问和修改类的属性,并执行任务以响应外部的调用。
类本身不占用内存空间,它只是一个创建对象时所依据的模板。
示例:定义一个简单的 Student 类,其中包含一些属性和方法。
class Student {
public:
string name;
int age;
void printInfo() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
在这个例子中,Student 类有两个属性(name 和 age)和一个方法(printInfo())。
对象(Object)
对象是根据类定义创建的实例。每个对象都拥有类中定义的属性和方法的副本。对象在内存中占用实际的空间,每个对象都可以拥有独立的属性值,而这些属性值定义了对象的状态。
当创建类的一个对象时,你可以为对象的每个属性提供具体的值,这些值可以通过对象的方法来操作和管理。
示例:基于 Student 类创建两个对象。
Student student1, student2;
student1.name = "Alice";
student1.age = 20;
student2.name = "Bob";
student2.age = 22;
student1.printInfo(); // 输出: Name: Alice, Age: 20
student2.printInfo(); // 输出: Name: Bob, Age: 22
在这个例子中,student1 和 student2 是 Student 类的两个对象。它们各自存储不同的数据,并独立调用同一个类定义的方法。
类与对象的关系
- 类定义了对象的结构:它指定了对象包含哪些数据和可以执行哪些操作。
- 对象是类的实例:每个对象都是基于类的定义创建的,具有类中定义的属性和方法。
10.this指针
this 指针是一个特殊的指针,它在每个非静态成员函数中自动可用,指向用来调用成员函数的对象。它允许对象的成员函数访问调用它们的那个具体对象的成员变量和其他成员函数。
下面是this指针的常用用法:
class Box {
public:
int length;
int width;
int height;
// 构造函数
Box(int length, int width, int height) {
// 使用 this 指针消除参数和成员变量的歧义
this->length = length;
this->width = width;
this->height = height;
}
// 成员函数,用于比较大小
bool isSameSizeAs(const Box& other) {
return (this->length == other.length &&
this->width == other.width &&
this->height == other.height);
}
// 返回*this,允许链式调用
Box& setSize(int length, int width, int height) {
this->length = length;
this->width = width;
this->height = height;
return *this;
}
// setSize 函数设置了 Box 对象的尺寸,并返回了当前对象本身(*this),使得可以直接在一条语句中继续调用 Box 的其他成员函数(即所谓的链式调用)。
// 显示盒子的尺寸
void displaySize() {
std::cout << "Length: " << this->length
<< ", Width: " << this->width
<< ", Height: " << this->height << std::endl;
}
};
int main() {
Box box1(10, 12, 5);
Box box2(10, 12, 5);
if (box1.isSameSizeAs(box2)) {
std::cout << "Box1 and Box2 are the same size." << std::endl;
}
// 链式调用
box1.setSize(5, 5, 5).displaySize();
}
11.友元
在 C++ 中,**友元(Friend)**是一种允许某些外部函数或其他类访问私有(private)或受保护(protected)成员的机制。友元关系是单向的和非传递的,即被授权访问的函数或类可以访问原类的非公开成员,但这不意味着原类可以访问对方的非公开成员,也不意味着友元的友元有相同的访问权限。
友元的类型
- 友元函数:
- 即使不是类的成员函数,友元函数也可以访问类的所有成员(包括private和protected)。通常用于实现需要访问类内部数据的辅助功能,但不适合作为类的成员的函数。
- 友元类:
- 友元类的所有成员函数都可以访问原始类中的私有和受保护成员。这通常用于实现具有紧密关联但逻辑上不属于同一类的功能的类。
为什么使用友元?
友元机制允许你控制哪些外部函数或类可以访问类的内部成员。这可以在不公开类内部实现细节的前提下,增加程序的灵活性和功能性。例如,可以定义一些与类协作的函数或类,使它们能够访问并操纵私有数据。
示例
下面是展示友元函数和友元类如何工作的简单示例:
#include <iostream>
class Box {
private:
double width;
public:
double length;
void setWidth(double wid) {
width = wid;
}
// 声明一个友元函数
friend void printWidth(const Box& b);
};
// 友元函数定义
void printWidth(const Box& b) {
// 直接访问私有成员
std::cout << "Width of box: " << b.width << std::endl;
}
class BoxManager {
public:
// 修改 Box 的 width(尽管是私有成员)
void updateWidth(Box& b, double wid) {
b.width = wid;
}
// 声明友元类
friend class Box;
};
int main() {
Box box;
box.setWidth(10.0);
printWidth(box); // 调用友元函数
BoxManager manager;
manager.updateWidth(box, 5.0);
printWidth(box); // 验证 width 是否已更改
}
在这个例子中:
printWidth是一个友元函数,可以访问Box类的私有成员width。BoxManager类想要修改Box的width,这通常是不允许的,因为width是一个私有成员。但如果BoxManager被声明为Box的友元类,则其成员函数可以访问和修改Box的私有和受保护成员。
12.类继承
继承是面向对象程序设计中最重要的一个概念。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。当创建 一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员 即可。这个已有的类称为基类,新建的类称为派生类。
类型的继承
C++ 支持几种类型的继承:
- 公有继承(Public Inheritance):最常用的继承方式。基类的公有成员和保护成员会成为派生类的公有成员和保护成员。
- 保护继承(Protected Inheritance):基类的公有和保护成员都会成为派生类的保护成员。
- 私有继承(Private Inheritance):基类的公有和保护成员都会成为派生类的私有成员。
继承的语法
在 C++ 中,你可以使用冒号(:)后跟访问说明符(如 public、protected 或 private)和基类名称来声明继承。
示例代码:
#include <iostream>
// 基类
class Animal {
public:
void eat() {
std::cout << "I'm eating generic food." << std::endl;
}
};
// 派生类
class Cat : public Animal {
public:
void meow() {
std::cout << "I can meow!" << std::endl;
}
};
int main() {
Cat myCat;
myCat.eat(); // 调用继承自 Animal 的方法
myCat.meow(); // 调用 Cat 特有的方法
return 0;
}
在这个例子中,Cat 类继承了 Animal 类。这意味着除了 Cat 类自己定义的方法(如 meow())外,Cat 类的对象还可以使用 Animal 类中定义的方法(如 eat())。
继承的特点
- 多态:继承是实现多态的关键。多态允许通过基类指针或引用来调用派生类的方法。
- 方法重写(Overriding):派生类可以重写(即提供新的实现)基类中的方法。
- 方法重载(Overloading):派生类可以定义一个与基类同名但参数不同的方法。
特殊成员函数的继承
- 构造函数和析构函数:不会被继承。派生类需要定义自己的构造函数和析构函数。如果派生类没有显式定义构造函数,会自动调用基类的默认构造函数。
- 复制构造函数和赋值运算符:如果派生类没有显式定义这些,编译器会生成默认的复制构造函数和赋值运算符,它们会调用基类的相应成员。
创建派生类对象时,程序会首先调用基类的构造函数,然后再调用派生类的构造函数,基类构造函数负责初始化继承的数据成员,派生类构造函数负责初始化新增的数据成员。
假设现在有一个工作是统计学校内学生的基本信息,我们首先要做的就是分析问题,当然就是统计学校内学生的基本信息,要使用面向对象的思想解决问题首先就要明确问题中的对象是什么关系,在这 里学校和学生显然是我们首要考虑的两个对象,由于学校包含学生,而学生不是学校,学校也不是学生,故为 has-a 关系而不是is-a关系,因此不能使用继承的关系。我们必须设计两个数据类型以分别存储学校和学生的信息,其中学校的数据类型中应包含学生类型,这样我们就有个整体的结构。接下来就是分析需求,要统计信息必然要输入信息,因此要有一个输入函数,同时还要有展示信息、统计人数等相关函数。最后,就可以编写代码了:
#include <iostream>
#include <string>
using namespace std;
const int MaxNum = 1000; // 最大允许人数
struct Student
{
string name_; // 姓名
string id_; // 学号
int gender_; // 性别 0->男 1->女
int grade_; // 年级
};
class School
{
private:
string school_name_;
Student students_[MaxNum];
int num_; // 学校实际人数
public:
School() {}
~School() {}
// 接口
School(string name, int num) : school_name_(name), num_(num) {}
void inputInfo(); // 输入学生信息
void show(); // 展示学生信息
void countGrade(int grade); // 统计不同年级人数
void countGender(int gender); // 统计不同性别人数
};
void School::inputInfo()
{
cout << "请输入学生信息(姓名、性别、学号、年级)\n";
for (int i = 0; i < num_; i++)
{
cin >> students_[i].name_ >> students_[i].gender_ >> students_[i].id_ >>
students_[i].grade_;
}
}
void School::show()
{
cout << "学校学生信息如下: \n";
for (int i = 0; i < num_; i++)
{
cout << students_[i].name_ << " ";
cout << students_[i].gender_ << " ";
cout << students_[i].id_ << " ";
cout << students_[i].grade_ << "\n";
}
}
void School::countGrade(int grade)
{
int count = 0;
for (int i = 0; i < num_; i++)
{
if (students_[i].grade_ == grade)
{
count++;
}
}
cout << "年级为" << grade << "的人数有: " << count << '\n';
}
void School::countGender(int gender)
{
int count = 0;
for (int i = 0; i < num_; i++)
{
if (students_[i].gender_ == gender)
{
count++;
}
}
cout << "性别为" << (gender ? "女" : "男") << "的人数有: " << count << '\n';
}
int main()
{
int num;
cin >> num;
School XiWang("XiWangZhongXue", num);
XiWang.inputInfo();
XiWang.show();
XiWang.countGender(1);
return 0;
}
13.模板
在 C++ 中,模板是一种强大的特性,用于支持泛型编程。通过使用模板,程序员可以编写与类型无关的代码,使得一个函数或一个类可以用于多种数据类型。这样可以增加代码的复用性和灵活性,同时还可以保持类型安全。
两种主要的模板
-
函数模板: 函数模板允许你定义一个操作不同类型的函数。编译器根据函数调用时提供的参数类型自动生成该特定类型的函数实例。一般形式如下:
template <typename type> ret-type func-name(parameter list) { // 函数的主体 }其中,type是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
示例代码:
template <typename T> T max(T a, T b) { return (a > b) ? a : b; } int main() { std::cout << max(10, 15) << std::endl; // 输出:15 std::cout << max(12.5, 9.2) << std::endl; // 输出:12.5 std::cout << max('a', 'z') << std::endl; // 输出:'z' }在这个例子中,
max函数模板用于比较两个值,并返回较大的值。它适用于任何支持比较操作的类型。 -
类模板: 类模板允许你定义可以操作多种数据类型的类。同样,编译器会根据用于类实例化的类型来生成具体的类。
示例代码:
template <typename T> class Box { private: T content; public: void setContent(T newContent) { content = newContent; } T getContent() { return content; } }; int main() { Box<int> intBox; intBox.setContent(123); std::cout << intBox.getContent() << std::endl; // 输出:123 Box<std::string> stringBox; stringBox.setContent("Hello, Templates!"); std::cout << stringBox.getContent() << std::endl; // 输出:"Hello, Templates!" }在这个例子中,
Box类模板提供了一种存储单一类型数据的方法。类可以根据需要实例化为任何类型,从整数到字符串。
14.重载
运算符重载
在C++中,运算符重载(Operator Overloading)是允许你为自定义类型(类或结构体)定义运算符的行为。通过运算符重载,可以让对象像内建数据类型一样进行运算符操作(如加法、减法、乘法等)。这使得代码更加简洁、易于理解和维护。
重载的运 算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的,如operator + 。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
我们可以在类的内部或外部定义一个函数来实现运算符重载,例如,在类的内部我们可以:
class Complex {
public:
int real, imag;
Complex(int r = 0, int i = 0) : real(r), imag(i) {}
// 重载加法运算符
Complex operator + (const Complex& other) {
return Complex(real + other.real, imag + other.imag);
}
};
在上面的例子中,我们为 Complex 类重载了加法运算符(+),使得两个 Complex 对象可以像内建类型一样通过加法运算符进行相加。
函数重载
函数重载是通过使用相同的函数名来定义多个不同的函数版本。重载函数的参数列表必须不同:可以通过改变参数的个数、类型或者顺序来实现重载。函数的返回值类型不能作为区分重载的依据,即两个函数如果仅仅返回类型不同,但参数相同,它们不能被重载。
例如,根据参数个数重载:
#include <iostream>
class Print {
public:
// 打印一个整数
void display(int a) {
std::cout << "Integer: " << a << std::endl;
}
// 打印两个整数
void display(int a, int b) {
std::cout << "Two Integers: " << a << " and " << b << std::endl;
}
// 打印一个字符串
void display(std::string str) {
std::cout << "String: " << str << std::endl;
}
};
int main() {
Print p;
p.display(5); // 调用 display(int)
p.display(10, 20); // 调用 display(int, int)
p.display("Hello"); // 调用 display(std::string)
return 0;
}
根据参数类型重载:
#include <iostream>
class Printer {
public:
// 打印整数
void print(int a) {
std::cout << "Printing integer: " << a << std::endl;
}
// 打印浮点数
void print(float a) {
std::cout << "Printing float: " << a << std::endl;
}
// 打印字符串
void print(std::string str) {
std::cout << "Printing string: " << str << std::endl;
}
};
int main() {
Printer p;
p.print(10); // 调用 print(int)
p.print(3.14f); // 调用 print(float)
p.print("Hello World"); // 调用 print(std::string)
return 0;
}
根据参数顺序重载:
#include <iostream>
class Printer {
public:
// 打印两个整数,顺序是先整数后浮点数
void print(int a, float b) {
std::cout << "Printing int and float: " << a << " and " << b << std::endl;
}
// 打印两个整数,顺序是先浮点数后整数
void print(float a, int b) {
std::cout << "Printing float and int: " << a << " and " << b << std::endl;
}
};
int main() {
Printer p;
p.print(5, 3.14f); // 调用 print(int, float)
p.print(3.14f, 5); // 调用 print(float, int)
return 0;
}