程序设计备考笔记

30k words

——Smiling

0 前言

本笔记写于2025年年初,为应对程序设计与数据结构-1的期末考试而写。主要针对的是在各类复习资料以及历年上交各班的期末考试试卷中出现的有关c++基本语法的易错点和考点,以供复习之用。

1 整形存储

1.原码(符号位+数值)

最高位为符号位,0表示正数,1表示负数,其余位表示数值的绝对值。

short int 类型的-1,其原码为 1000000000000001

2.反码

正数的反码不变。负数的反码符号位不变,其余位取反
short int 类型 -1 的反码为 1111111111111110(原码除符号位外按位取反)

3.补码

正数的补码与原码相同。
负数的补码是在反码的基础上加 1。
short int类型-1的补码为 1111111111111111,在内存中存储为0xFFFF(十六进制表示的补码形式)。

2 运算符

1.优先级

image-20250110183458017

==!最高,(),算术运算符,关系表达式(>,!=等),&&,||,赋值运算符(=,+=,*=等)最低==

2.

大多数运算符都可以重载,但有一些运算符是不能重载的:

  • 成员访问运算符(.
  • 成员指针访问运算符(.\*->\*
  • 作用域解析运算符(::
  • 条件运算符(?:
  • sizeof 运算符(sizeof
3.

C++提供的运算符按照运算数的个数分为两种:一元运算符和二元运算符(X) 还有三元运算符:?:表达式

3 构造函数

使用类的时候,在A p;或者A p = new A(..)的时候会调用构造函数,但是在A *p1;的时候是不会的,只是建了一个指针,没有新建类对象,在之后p1 = new A(…)的时候才会。

判断:对象作为实参时系统将自动调用拷贝构造函数(X)

值传递会调用拷贝构造函数,传递引用/指针的时候不会调用

后两种直接修改原始对象,拷贝构造不会修改原始对象(修改的是一个副本)

三种特殊情况:

1. 基类构造函数需要参数,派生类本身不需要构造函数时

  • 派生类必须定义构造函数:当基类的构造函数需要参数时,派生类必须定义自己的构造函数,即便派生类自身不需要进行额外的初始化操作。
1
2
3
4
5
6
7
8
9
class Base {
public:
Base(int x) { /* 基类构造函数,需要参数 */ }
};

class Derived : public Base {
public:
Derived(int x) : Base(x) { /* 派生类构造函数,仅用于传递参数给基类 */ }
};

2. 基类使用缺省或不带参数的构造函数

  • 派生类构造函数初始化列表可略去基类构造函数名(参数表)
1
2
3
4
5
6
7
8
9
class Base {
public:
Base() { /* 基类缺省构造函数 */ }
};

class Derived : public Base {
public:
Derived() { /* 派生类构造函数,无需在初始化列表中显式调用基类构造函数 */ }
};

3. 省略派生类构造函数的情况

1
2
3
4
5
6
7
8
class Base {
public:
Base() { /* 基类默认构造函数 */ }
};

class Derived : public Base {
// 未定义构造函数,编译器会生成默认构造函数,该函数会调用基类的默认构造函数
};

4 析构函数

A &b = a;

则b是a的引用,在a析构的时候b也就没了。所以在程序结束的时候,只会调用a的析构函数。

A a或者A *a = new A等都是需要析构的,无论是否是new出来的

构造函数和析构函数都没有返回类型,不等价于返回void!这是两个完全不同的概念。

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

析构函数没有参数

先构造,后析构;后构造,先析构!构造先基类后派生类,析构先派生类后基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
~A(){};
}
class B:public A{
~B(){};
}
//在析构派生类对象的时候,会先调用派生类的析构,再调用基类的析构
class A{
virtual ~A(){};
}
class B:public A{
~B(){};
}
//在调用对于指向派生类对象的基类指针时,需要用到虚析构函数,先调用派生类的析构,再调用基类的析构

全局变量和静态局部变量是同等地位,谁先析构看谁后构造

1
2
3
4
5
6
class A{};
A a;
int main(){
static A b;
}
//程序结束之后,b先析构,然后是a

5 冯诺依曼体系结构

冯诺依曼结构计算机的五大部分:
1、输入数据和程序的“输入设备”;
2、记忆程序和数据的“存储器”;
3、完成数据加工处理的“运算器”;
4、控制程序执行的“控制器”;
5、输出处理结果的“输出设备”。

没有编译器。

6 Protected、Private和Public

Protected可以被派生类直接访问,而Private不可以

public继承:基类的Public和protected仍然如此,基类的Private不能访问

Private继承:基类的Public和protected变成Private,基类的Private不能访问

protected继承:基类的public和protected变成protected,基类的private不能访问

7 Switch

switch-case中,case不可以相同

8 指针的使用

1.
1
2
3
4
5
6
7
8
9
int a[3]={135},*p;
int i, b[6];
p = a;
b[0]= *p;//b_0 =1
p++;b[1]=*p;//b_1=3
b[2]=(*p)++;//b_2 = 3;a_1 变为4
b[3]=*(p-1);//b_3 =1;(地址向前走一个)
b[4]=(*p)+1://b_4=4+1=5(对于这个地址的值+1)
b[5]=*(p+1);//b_5=a_2=5;
2.

指针在没有赋初值的时候不可以直接使用,例如,

1
2
3
4
studentT student1, *sp;
sp->chinese=90;
(*sp).chinese=90; //注意看看这种用法哦,在赋了初值的时候这种用法常常被误以为是错的
sp.chinese=90

这三种使用都是错的

9 虚函数的使用

1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base{
public:
virtual int f1(){ return 100;}
void f2(){ cout<<"Base"<<endl;}
}
class Derived:public Base{
public:
int f1(){ return ; }
void f2(){ cout<<"Derived"<<endl; }
};
int main()
Derived d;
Base *b=&d;//用指针来调用d
Base &c=d;//用引用来调用d
Base x;
cout<< x.f1()<< endl;//x是base,输出100
cout<<b->f1()<<endl;//f1虚函数,所以调用d实际的f1,0
b->f2();//f2不虚,调用base,输出base
cout<<c.f1()<<endl;//f1虚函数,调用d实际的f1,0
return 0;
}
2.
1
2
class A {public: virtual void display(); }; 
class B:public A { public: virtual void display();};

A *p = B &b.在调用display的时候会根据指针实际指向的对象类型来决定调用哪个函数,即:调用B的display
引用也一样:A &p = b,也会调用B的display
A p = B b;这里发生了切片,b的B部分被切掉,只有A部分被复制给a。因此,a.display()调用的是A类的display函数

3.
1
2
3
4
5
6
7
8
9
10
11
12
13
classA {
public:
virtual void f1();
void f2();
};
class B:public A {
public:
virtual void f1();
void f2();
};
A *pa = new B;
pa->f1();//f1是虚函数,调用的时候用派生类中的
pa->f2();//f2不是,调用的时候根据指针的类(A),调用基类的
4.教材中的虚函数内容(使用方法以及override,final,虚析构函数,纯虚函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
public:
virtual void f1(int);
virtual void f2();
}
class B:public A{
public:
void f1(int);
void f2(int);
}

A *p1 = new A;//基类指针指向基类对象
A *p2 = new B;//基类指针指向派生类对象

p1->f1(2);//正常执行基类的f1
p2->f1(2);//p2是基类指针,只能看到基类的f1,然后发现f1是虚函数!于是检查p2到底指向谁,p2指向的是派生类对象,于是找到派生类中的f1执行
  • override的使用

    显式指定覆盖,让编译器帮你检查一遍覆盖的函数原型是否和虚函数一致,如果不一致则报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A{
    public:
    virtual void f1(int);
    virtual void f2();
    }
    class B:public A{
    public:
    void f1(int) override;//正确,成功覆盖
    void f2(int) override;//错误,原型不同,编译器报错
    int f3(int) override;//错误,报错
    }
  • final的使用

    将某个虚函数指定为final,表示它的派生类中不允许覆盖该函数。表示该虚函数在当前类中是最后一个实现,不能在子类中被重写,可用于控制虚函数的重写行为。

    1
    2
    3
    4
    5
    6
    7
    8
    class A{
    public:
    virtual void f1 final (int);
    }
    class B:public A{
    public:
    void f1(int);//此时,编译器会报错(但是这样的final是没有意义的)
    }

    一般的使用场景是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A{
    public:
    virtual void f1 (int);
    }
    class B:public A{
    public:
    void f1 final (int);//另外一个要注意的点:派生类中的f1会默认也是虚函数——虚函数具有继承性
    }
    class C:public B{
    public:
    void f1 (int);//报错,这表示虚函数到B就截止了,不可以在更深的子类里重写,保证安全
    }
  • 虚析构函数

    构造函数不可以是虚函数,析构函数最好是虚的,因为当我们使用基类指针指向派生类对象时,调用析构函数的时候,基类指针只能看到基类的析构函数,这样析构的时候就只能析构掉基类的部分。而如果使用虚析构函数,那么在析构时就会到实际的派生类中去调用析构函数,防止内存泄漏。

  • 纯虚函数

    1
    virtual void f1 (int) = 0;

    在基类中,用等于0标识,说明这个函数在每个派生类中实现,这个基类成为抽象类,不能定义抽象类的对象,只能定义抽象类的指针。这样基类成为一个基底,不能使用,但作为所有上层建筑的框架。

10 左右值

如有函数f(A &x, const A &y),并有定义 A b(2),调用f(4, b)时,A的构造函数的调用次数是 0(re)

1
2
3
4
void f(A&x,const A&y){...}
A b(2);
f(4,b);//报错!4输入是一个右值,&需要一个左值(引用表示可更改---记忆)
//修改:f的第一个参数传入改为A&& x或者const A& x;就可以了

11 坑

1.=和==:

如果题目中的判断条件是“=”,仔细看!可能不是想象的那样

2.保护符

程序改错的时候,要注意:如果有多个cpp包含了一个头文件,要注意这个头文件有没有保护符

3.vector迭代器

vector在扩容(比如push_back)的时候,所用的迭代器都会失效,所以不能

1
2
3
4
vector<int> a;
auto x = a.begin();
a.push_back(1);
if(a.end >= x){}
4.!

!(x%7)和x%7!=0不同,!(x%7)中,如果x是7的倍数,x%7=0,!0=true,反而是true

5.&&

留意短路特性,&&如果前面一个已经是false了,后面一个是不会算的,||同理。

注意&&和&(与)是不一样的,前一个是逻辑运算符,后一个是不进位的二进制加减(负数:转换成补码之后按照相同形式进行运算,注意:符号位也一样)

6.e

e是浮点数类型的表示方式,1e2是一个double型

7.字符串\0

在常量字符串输出的时候,遵循的规则和字符数组一样,见到\0就会停止,例如

1
cout << "abc\0abc";//会输出abc
8.std::

是否要加std::,比如vector加没加

9.把对象作为参数/返回值

作为参数:

如有函数:void f(IntArray array2);函数调用:f(array1);
一个局部变量array2作为形参,并用主调本质上是在被调函数中定义函数中已有的实参对象array1来初始化这个新定义的形参对象。所以会调用拷贝构造函数
相当于执行了:IntArray arry2 = array1;

作为返回值:

如有函数IntArray f(){IntArray a;...return a;}
当执行到return语句时,会在主调函数中创建一个IntArray类的临时对象作为返回值,并调用拷贝构造函数,用对象a来初始化该临时对象,
相当于执行了:IntArray 临时对象=a

10.二维数组

二维数组的第二个位置的大小必须指定:a[][5]

等价于a[i][j]的表达式写法:

*(a[i] + j)

(*(a+i))[j]

*(*(a+i)+j)

*(&a[0][0] + 5 * i + j)

11.return

return后面的表达值的类型一定是函数的返回类型(半对)可能存在隐式类型转换

12.类的构造
1
2
3
4
5
6
7
8
class Array{
int n;
int *a;
public:
Array(int num):n(num){
a = new int [n * n];//注意这里不要随手一写写成int* a = new int [n * n];了,a已经定义了!所以这里要做的是修改,而不是又构建一个局部变量
}
}

12 智能指针

shared_ptr<A> p3( new A), p4 = p3; 在析构时只会调用一次析构函数,因为他们指向的是同一个对象

13 指针与字符串

1.例题

若有定义 :char str1[5];const char *p=“abc”;下列不正确的语句是

A. p=“def”;

B. strcpy(p, str1);

C. p=str1;

D. strcpy(str1,p);

A:把 def所在的地址传给了p,修改了p指针指向的地点。字符串在编译的时候自动转换为了其所在的起始地址。
B:想直接修改p指向的内容,与const 冲突 B

2.打印

string s1 = “abc”

cout << s1: abc

当你使用 cout << s1 时,s1 被识别为一个以 null 结尾的字符数组(或者是一个指向 const char 的指针),因此,cout 会打印出 s1 指向的字符串,即 “dbbbcaaa”。

cout << (void *)s1: 0x100

当你使用 cout << (void *)s1 时,s1 被显式地转换为 void 指针,这通常意味着 cout 将打印出 s1 的地址(即 s1 所指向的内存地址的数值表示)。这个地址将以十六进制形式打印出来,而不是字符串的内容。

cout << *s1: a

这里 *s1 是对 s1 指针解引用的结果,它获取了 s1 指向的第一个字符。因此,cout 会打印出 s1 指向的第一个字符,而不是整个字符串。

3.常量存储区

char *s=”hello sjtu”时,*s指向的一块常量存储区,通常是只读的,不能通过strcpy函数去修改它所指向的内容,例如,不可以strcpy(s,"sjtu"),但如13.1所述,s = "sjtu"就是可以的(把一个新的常量存储区里的地址给了s)

14 fstream

在C++中,fstream 类型的对象可以使用以下几种调用模式来打开文件:
1.ios::in-打开文件用于输入(读取)。
2.ios::out-打开文件用于输出(写入)。如果文件不存在,它将被创建。
3.ios::app-打开文件用于追加数据。写入操作总是在文件末尾进行。
4.ios::ate-打开文件时,文件指针被定位到文件末尾。
5.ios::trunc-如果文件已经存在,则打开文件时将其长度截断为0(即删除文件内容)。
6.ios::binary-以二进制模式打开文件。这与文本模式相对,文本模式会根据操作系统的需要进行字符转换(例如,在Windows上,可能会将换行符\n转换为r\n)。
这些模式可以单独使用,也可以通过位或操作(I)组合使用。以下是一些组合式的例子
ios::in|ios::out-打开文件进行读写操作。
ios::out|ios::trunc-打开文件进行写入,并截断文件(如果文件已存在)

ios::in|ios::out|ios::binary-以二进制模式打开文件进行读写操作。
当不指定任何模式时,默认的模式是 ios::in |ios::out ,这意味着文件被打开用于读写操作。

写进去:ios::out

读取:ios::in

Rmk:记忆:in是读到电脑里,所以是读取,out是从电脑抛到文件中,所以是out

Binary一般需要和in或者out一起调用,否则没有读取或写入权限(废)

用法:fstream file1("example.txt", ios::in);,和学的ofstream和ifstream多一个参数

fstream的默认打开方式是ios::in|ios::out,

ofstream的默认打开方式是ios::out,

ifstream的默认打开方式是ios::in

15 抽象类

抽象类(有纯虚函数virtual void print() = 0;,没有函数体,=0标识):

不可以被实例化,但是完全可以作为派生类或者基类,

抽象类相当于一个接口,具体实现只能在派生类中实现,也不可以尝试去调用

此时,不可以尝试着构建一个A x,(假设A是抽象类),只能通过派生类构造

16 重载输入输出函数

1
2
3
4
5
6
friend ostream& operator<<(ostream &os, const foo &obj)   //返回的应该是原样的os,是引用,要用&
{
os << '(' << obj.a << ',' << obj.b << ')';
return os;
}
friend istream& operator>>(istream &is, foo &obj)

注意格式(第一排)

可以写在类里(但一般写在类外),记得要用友元函数。

operator@运算符重载(=,.,[],()必须重载为成员函数,流插入符号为全局函数(友元)

原因:如果将其重载为类的成员函数,那么调用方式就会变成obj << cout,这与我们通常使用的cout << obj的习惯不符。

17 异常

1
2
3
4
5
6
7
for (int i = 1; i < 6; ++i)
try {
cout << f(i) << endl;
}
catch(dataException) {
cout << "equal\n";
}

在这段代码中,如果在f(3)的时候抛出异常,则会进入catch块,但是不会出for(catch结束之后,进行下一次循环)

1
2
3
4
5
6
7
8
9
10
setTry newOne[3];
try{
for(int i=0;i<3;i++)
{
newOne[i].do();
throw(i);
}
} catch (int i) { cout << i<<','; }
cout << "6,";
return 0;

但是,在这个里,由于catch是不在for里面的,所以catch之后直接走人,不再继续for

18 模版

在C++中,模板包括函数模板与类模板

类模板的成员函数可以是普通函数,不可以是函数模板

函数模版的格式:

template < 模板参数列表 >返回值函数名 (函数参数列表)

eg.template <class T> T square(T x) { return x * x; }

使用的时候直接 square(100)即可

类模板的非类型模板参数要求在编译期就能确定其具体的值,必须是常量表达式,例如:

1
2
3
4
5
6
7
template <class T, int L>
class foo {};
class A{};
int con = 10;
foo<int,con> obj1; //错误,con是一个变量,在编译的时候无法知道是什么值(如果改成const int con = 10的话就没错了)
foo<A, con> obj2; //错误同上
foo<int, 5> obj3; //正确

19 静态成员

静态数据成员是类的所有对象共享的成员,它不依赖于任何对象而存在,在程序开始执行时就会被分配内存空间,所以在建立对象前,就可以为静态数据成员赋值,比如可以通过类名加作用域运算符::来对静态数据成员进行赋值。

eg.

1
2
3
4
5
class A{
public:
static int a;
}
A::a = 10;//在类外定义时,不加static前缀

静态成员函数既可以在类外定义,也可以在类内定义,在类内定义时,隐式地内联(只有static const int这种整型静态常量可以)。不可以放在类的构造函数里初始化。(结合46看)

this指针指向的是调用成员函数的对象,而静态成员函数不属于任何一个具体的对象,是属于整个类的,所以在静态成员函数中不能使用this指针.

在使用静态成员的时候,既可以使用A::a,也可以使用obj.a

静态成员函数一样,

1
2
3
4
5
6
7
8
9
10
class A{
public:
static void a();
};
void A::a(){};
//两种使用方式:
A::a();

A obj;
obj::a();

派生类中可以定义名字一样的静态成员,但不会覆盖,只是在调用时默认调用派生类的,也可以用基类::的方式调用基类的静态成员

static 成员函数不得调用非 static 成员

20 extern关键字

表示在另一个文件中定义。

21 转义字符

‘\012’代表八进制下的012,也就是10,’’括起来会变成Asc2下的相应字符

‘\x12’代表十六进制下的12,也就是18,’’括起来会变成Asc2下的相应字符

‘\0’是null(空字符)

转义字符只占一个字节

22 初始化

对于数组,会自动补充0的:

第一种,全局或者静态变量;

第二种,显式初始化,初始化的元素个数小于长度时(int a [5]= {0};)。

其余情况都会是随机值

23 static

作用:

1.修饰局部变量,使得该局部变量的生命周期从此处延长到程序结束,但变量的作用域仍然不变,在他所在的函数/…内,不可以在之外直接调用。

2.修饰全局变量/全局函数,使得该变量/函数不能被其他文件访问,就算使用extern也不可以,且静态全局函数不能直接访问其他函数中的非静态局部变量,但可以将他们作为参数传给静态函数

3.修饰类的成员:见19

必须要在类外定义,但可以不初始化(默认初始化为0),类内只是声明了。

  • 未被程序员初始化的静态变量都由系统初始化为0
  • 局部静态变量在编译时赋初始值的,当运行时重复调用此函数时,不重复赋初始值
  • 虽然局部静态变量在函数调用结束后仍然存在,但其他函数不能引用它

24 友元

类的友元不具有传递性,若类A把类B设为友元,则B可以访问A的私有成员

类A的成员函数作为类B的友元函数时,必须先声明类B,再定义类A,然后定义类B(声明友元类时,情况类似)

25 合法的变量名

合法的变量名称必须以字母或下划线开头,只能包含字母、数字或下划线,且不能是C++ 的保留字(keywords)

26 会写冒泡排序

1
2
3
4
5
6
7
8
9
for(int i = 0 ; i< n ; i++){
for(int j = 0 ; j < n - i - 1;j++){
if(a[j] > a[j + 1]){
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}

27 const

在一个函数中,如果调用了自己建的类,如void func(const A& a){},const说明不会更改a的内容,那么就只能调用A类里面承诺不会更改成员的函数,如void get() const {...},这个const的位置,表示不会修改成员变量,注意const不是加在最前面的,加在最前面,如const int get(){...}表示的是返回值不能修改(不会报错,但基本不会这么用)

const 数据成员只能初始化列表,不能赋值

对const对象只能调用const成员函数,且必须初始化

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass {
private:
int data;

public:
// 非const成员函数,可能修改data
void modifyData(int newData) {
data = newData;
}

// const成员函数,承诺不修改data,请务必注意const的位置!是在后面的
void printData() const {
std::cout << "Data: " << data << std::endl;
}
};

int main() {
const MyClass obj(5); // 常量对象不能被赋值,只能初始化,且一定要初始化
// obj.modifyData(10); // 错误,不能调用非const成员函数修改const对象
obj.printData(); // 正确,只能调用const成员函数
return 0;
}

const指针:

区分方法:去掉类名之后看const的右边有没有*,如果有,说明不能改变的是值,没有,说明不能改变的是指针本身(指向性)

1. 不许改变指针本身(己值和他址):int* const p;
  • 含义:这种声明方式表示 p 是一个常量指针。一旦 p 被初始化指向某个地址,就不能再让它指向其他地址了。但是,通过 p 可以修改指向的对象的值。
  • 示例
1
2
3
4
5
int a = 5;
int b = 10;
int* const p = &a;
// p = &b; // 这是错误的,不能修改指针 p 本身,使其指向 b
*p = 20; // 这是正确的,可以通过 p 修改 a 的值为 20
2. 不许改变指针所指的对象(他值):const int *pint const *p;
  • 含义:这两种声明方式表示 p 是一个指向常量的指针。也就是说,不能通过 p 去修改它所指向的对象的值,但 p 本身可以被重新赋值,指向其他的地址。
  • 示例
1
2
3
4
5
int a = 5;
int b = 10;
const int *p = &a;
// *p = 20; // 这是错误的,不能通过 p 修改 a 的值,因为 p 指向的是常量
p = &b; // 这是正确的,可以让 p 重新指向 b

辨析:

1
2
3
4
const int a = 10 , *p = &a;//合法,定义一个常量a之后,用一个const int *(不会修改指向对象的值的指针)来指向a
const int a = 10; int *p = &a;//非法,用一个可能修改对象的值的指针来指a
int b; const int &a = b;//合法,a是一个常量引用,通过这个引用不能修改所绑定对象的值
const int a = 10 , &b = a;//合法,引用可以绑定到const对象上,并且一旦绑定就不能再通过该引用去改变所引用对象的值

28 类型转换

一般来说,类型转换是朝着能够容纳更多信息或者更通用的类型方向进行的。int 类型比 char 类型能表示更大范围的值,所以在这种算术运算中,char 会向 int 转换,而不是反过来将 int 转换为 char

所以,在auto a = ‘a’ + 0x10的时候,a会被转换为int类型,而不是把0x10转换为char类型

  1. long double>double>float
  2. long long>long>int>short>char
  3. 无符号类型>有符号类型

29 decltype关键字

decltype用于推导一个变量的类型,使用例子:int x; decltype (x) y; //此时相当于构造了一个int类型的y

30 字符串和字符数组

1
2
char str1 [] =  {‘h’, ’1’, ‘2’, ‘a’, ‘b’}, str2[4];
strcpy(str2, str1) ;

在这里,由于char str1是没有’\0’作为结尾的,所以他并不会自己结束,只能称为字符数组,而不能叫做字符串,当cout << str1 的时候输出内容是不确定的。同样的,对于str2,在strcpy的时候并不会自动帮str2加上一个’\0’,所以这也只能算是一个字符数组,在输出的时候结构式不确定的。

1、 初始化:char a[] = {'a','1','2','\0'}; char a[] = {"a12"}; 自动分配4个字节,自动在末尾添加‘\0’. char a[] = "a12";

2、 只有初始化时可以这样写,赋值时可不行:char a[10]; *a = "a12"; 错!因为*a只能存一个字符,要用strcpy(a,"a12");

31 constexpr关键字

constexpr用于指定函数或变量在编译时就可以求值(不是运行时)

constexpr修饰的变量必须在编译时就有确定的值,它隐含了const属性,即声明的变量是常量,不可修改。例如:constexpr int num = 10;num就是一个编译时常量,在编译阶段就确定了其值为 10。

1
2
3
4
5
constexpr int a = 10;
constexpr int b = a + 5;//合法
auto b = a;//b会被推导为int,constexpr只是修饰
int c = 5;
constexpr d = c + a;//这里c不是constexpr,要在运行的时候才能知道是什么,所以这是不合法的

constexpr函数是指能用于常量表达式的函数,其函数体在编译阶段就可以被求值。

函数的返回值类型必须是字面值类型,函数体中只能有一条return语句,且return返回的表达式必须是常量表达式或可在编译时求值的表达式。例如:

1
2
3
constexpr int add(int a, int b) {
return a + b;
}

32 拷贝构造和移动构造

拷贝构造:A(const A & other);常量左值

移动构造:A(A && other);右值

在对象之间互相赋值时会自动调用拷贝构造函数 MyClass(const MyClass& other);(x)

会调用赋值运算符重载函数:MyClass& operator=(const MyClass& other);

对象之间的赋值与拷贝构造无关,注意避免混淆

1
2
Rational r1 = r2;// 拷贝构造
Rational r1; r1 = r2;// 先构造,后赋值,没有拷贝构造

33 数组与指针

int a[3][3] = {1,2,3,4,5,6,7,8,9};中,a是一个二维数组名,它可以被看作是指向包含 3 个int元素的一维数组的指针,其类型为int (*)[3]a代表了整个二维数组的起始地址,a + 1指向下一的起始地址。a[1]*(a+1)是一个意思,可以int *p = a[1]

34 后置自增和前置自增

1
2
3
4
5
int &f(  int  &k1 )
{
static int k2 = ++k1;
return ++k2 ;//k2++不可以
}

注意这段代码中的返回部分,要返回一个引用!

  • ++k2:前置自增运算符 ++k2 会先将 k2 的值加 1,然后返回 k2 本身的引用。
  • k2++:后置自增运算符 k2++ 会先返回 k2 的值,此时返回的是一个临时对象,它是 k2 的原始值的副本,然后再将 k2 的值加 1。当函数返回这个临时对象的引用时,由于临时对象在表达式结束后就会被销毁,导致返回的引用指向了一个已经不存在的对象,这就产生了悬空引用,会引发未定义行为。

35 缺省值

People(char *n = nullptr, int day) //这个缺省值不符合语法规范!People(1)是不可以的

函数参数中一个参数有缺省值,后面参数都必须有缺省值,否则无法利用到这个缺省值(缺省值必须全部后置,把所有没有缺省值的参数尽量往前放)

36 模版形参

1
2
3
4
5
6
7
8
template <int LEN>
class A {
private:
People data[LEN];
};
int num;
A<num> array(name, day);//错误!模版形参LEN不可以传入一个变量,必须是一个确定的值,例如10

37 将派生类对象隐式转换为基类对象(多态,教材)

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
class A{
int x;
public:
A(int x1 = 0){x = x1;}
void display() const {cout << x << endl;}
};
class B:public A{
int y;
public:
B(int x1 = 0,int y1 = 0):A(x1){y = y1;}
void display() const {cout << y << endl;}
};

//派生类赋予基类
B b(1,2);
A a;
a = b;//这里会直接把b中基类的部分(x)给a,其余抹去

//指针
A *bp = &b;//基类指针bp指向一个派生类对象,只能解释这个派生类当中基类的部分
bp->display();//这里也只能使用基类中的display函数

//引用(实际上就是隐式的指针)
A &br = b;//相当于给派生类对象b的基类部分取了一个名字br,对br的访问和修改就是对于b的基类部分的访问和修改
br.display();//也是只能使用基类中的display函数

A *a;
B *b;
a = b;//切片
b = a;//报错,因为这不一定是正确的,所以编译器不会自动调用隐式类型转换
a = (A*) b;//显式的切片
b = (B*) a;//显式类型转换,这里可以成功是因为有可能a指向的是一个B类对象。但此时需要程序员承担这个风险

38 常引用

const A &k2 = k1;这一步相当于是把k1此时的样子记录下来了,之后无论k1怎么变化,k2始终是此时的样子

39 文件输入输出以及命令行

int argc, char *argv[]用于命令行输入,argc是参数个数,argv是每个参数

注意头文件:

1
2
3
4
5
6
7
8
9
10
#include<fstream>//最大
//读取,输入
#include<ifstream>
ifstream inFile("test.txt");
cout << inFile;

//写入,输出
#include<ofstream>
ofstream outFile("test.txt");
cin >> outFile;

文件输入输出例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class encrypt { 
protected:
ifstream in;
ofstream out;
public:
encrypt(const char *infile, const char *outfile) {
in.open(infile);
out.open(outfile);
}
virtual ~encrypt() {
in.close;
out.close;
}
};

std::ofstream out("data.txt"); 或者out.open(outfile);之后写入内容,是会直接覆盖原本的内容的,不是插入,是覆盖!

40 内存空间

堆内存空间

  • 定义:堆是程序运行时动态分配内存的区域,用于在程序运行期间根据需要动态地分配和释放内存。

栈内存空间

  • 定义:主要用于存储函数的局部变量、函数参数、返回地址等。
  • 特点:自动分配和释放

全局 / 静态存储区

  • 定义:用于存储全局变量和静态变量。
  • 特点:在程序编译时分配内存,程序结束时释放,其内存空间在程序运行期间保持不变。

常量存储区

  • 定义:专门用于存储常量数据,如字符串常量、数值常量等。常量存储区中的数据在程序运行过程中不能被修改。
  • 特点:常量存储区的内容在编译时确定,在程序运行期间保持不变,多个相同的常量可能会被合并存储,以节省内存空间。

41 文件的随机访问

获取文件定位指针的当前位置 :成员函数tellg和tellp

设置文件定位指针的位置:成员函数seekg和seekp

seekg和seekp都有两个参数:

第一个参数通常为long类型的整数,表示偏移量的字节数;

第二个参数指定寻找方向

​ ios::beg(默认):相对于流的开头

​ ios::cur:相对于流当前位置

​ ios::end:相对于流结尾

seekg:全称是 “seek get”,主要用于输入文件流(ifstream),用于设置文件读取指针的位置。

seekp:全称是 “seek put”,主要用于输出文件流(ofstream),用于设置文件写入指针的位置。

tellgtellp分别用于获取文件读取指针和写入指针的当前位置,和上面两个相对应。

例子:

1
2
3
4
5
6
in.seekg( n );//绝对位置
in.seekg( 2, ios::cur );//偏移
in.seekg( 0, ios::end );
//成员函数tellg和tellp分别返回get和put指针的当前位置
std::streampos currentPos = in.tellg();
std::cout << "当前读取指针位置: " << currentPos << " 字节";

42 手写链表

1
学!一定要手写几遍。

43 cstring

image-20250111213453212

44 swap的两种方式

指针参数方式

1
2
3
4
5
6
void swap(int *m, int *n) {
int temp;
temp = *m;
*m = *n;
*n = temp;
}

调用:swap(&x, &y)

注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void myfun(int*q , int &n) {
cout << *q << endl;
q = &n;
cout << *q << endl;
}
int main(){
int x = 0 , y = 1 , z = 2;
int *p = &x;
myfun(p,y);//在这里,传入p之后,函数内部的q = &n仅仅是把q指向的地址给换了,由于是值传递,所以无法在出了函数之后继续保持
//而在swap函数中,进行了解引用,虽然是值传递,但是他直接把*m所在地址的值给改了,让交换的目的达到
cout << *p << endl;
myfun(&z , *p);
return 0;
}

引用参数方式

1
2
3
4
5
6
void swap(int &m, int &n) {
int temp;
temp = m;
m = n;
n = temp;
}

调用:swap(x, y)

需要注意的是,函数使用引用参数时,实参必须是变量,而不能是一个表达式。

45 返回指针或引用

当函数返回值是指针时返回地址对应的变量不能是一个局部变量。

函数的返回值也可以是一个引用。它表示函数的返回值是函数内某个变量(不能是局部变量)的引用。

返回引用之后,可以这样:

1
2
3
4
5
int a[5] = {1,2,3,4,5};
int& func(int j){
return a[j];
}
func(3) = 5;//引用使之成为左值,能被直接修改

46 类的声明与赋初值

类的定义只是声明了成员,而没有定义成员,所以不能给数据成员赋初值

1
2
3
4
5
6
7
8
class 类名 {
int x = 0; // 错误!
int a[5] = {1, 2, 3, 4, 5}; // 错误!
int *p = new int[10]; // 错误!
const double PI = 3.1416; // 错误!
static double rate = 0.012; // 错误!
static const int MAX = 100; // 正确(这是唯一的例外)
};

47 explicit关键字

1. 作用

  • 防止隐式类型转换:当一个构造函数被声明为explicit时,它不能用于隐式地将其他类型转换为该类的对象。这可以避免一些意外的、可能不符合程序员意图的类型转换,使代码更加清晰和安全。

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
public:
// 普通的单参数构造函数
MyClass(int value) : m_value(value) {}
int getValue() const { return m_value; }
private:
int m_value;
};

int main() {
MyClass obj1(5); // 正常的构造函数调用
MyClass obj2 = 10; // 隐式类型转换,将10转换为MyClass对象,c++11之后只调用一次默认构造函数就够了

// 下面这种情况可能会出现意外
void func(MyClass obj);
func(20); // 这里会隐式地将20转换为MyClass对象并传递给func函数,可能不是程序员期望的行为

return 0;
}

如果将构造函数声明为explicit

1
2
3
4
5
6
7
class MyClass {
public:
explicit MyClass(int value) : m_value(value) {}
int getValue() const { return m_value; }
private:
int m_value;
};

此时,MyClass obj2 = 10;func(20);这样的代码将无法编译通过,因为explicit禁止了这种隐式类型转换,必须显式地使用MyClass obj(10);这样的方式来创建对象或调用函数。

48 词汇

1. hex16oct8dec10

  • 含义
    • hex16:表示十六进制格式,oct8:表示八进制格式,dec10:表示十进制格式
  • 示例
1
2
3
4
int num = 255;
cout << "十六进制: " << hex << num << endl;
cout << "八进制: " << oct << num << endl;
cout << "十进制: " << dec << num << endl;

2. setbase()

  • 含义:用于设置输出整数的进制基数。
  • 示例
1
2
int num = 100;
cout << setbase(16) << num << endl;

3. fixedsetprecision().precisionscientific

  • 含义
    • fixed:用于设置浮点数以定点数形式输出,即显示固定小数位数的浮点数。
    • setprecision():用于设置浮点数输出的精度(即小数位数),需要包含<iomanip>头文件。
    • .precision:是ostream类的成员函数,也用于设置浮点数输出的精度。
    • scientific:用于设置浮点数以科学计数法形式输出。
  • 示例
1
2
3
double num = 123.456789;
cout << fixed << setprecision(2) << num << endl;
cout << scientific << num << endl;

上述代码中,fixed结合setprecision(2)num以定点数形式输出且保留两位小数,scientificnum以科学计数法形式输出。

4. width()setwsetfill

  • 含义
  • 后两个需要 #include<iomanip>
    • width():是ostream类的成员函数,用于设置输出字段的宽度。
    • setw:是<iomanip>头文件中的函数,功能与width()类似,用于设置输出字段的宽度。
    • setfill:用于设置填充字符,当输出字段宽度大于要输出的内容宽度时,用设置的填充字符填充剩余宽度。
  • 示例cout << setw(10) << setfill('*') << 123 << endl;

在这个例子中,setw(10)设置输出宽度为10setfill('*')设置填充字符为*,输出123时会在前面填充*使总宽度达到10

49 有delete要有拷贝控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
int a;
int *pa;
public:
A(int x, int y = 10) { a = x; pa = new int(y); }
~A() { delete pa; }
A(const A& other) : a(other.a), pa(new int(*other.pa)) {}//一定要有这个,否则
//编译器会自动生成的拷贝构造是 A(const A& other) : a(other.a), pa(other.pa) {} ,导致两个pa指向同一个地址
};


A k1(1);
const A &k2 = k1;

在代码中,类 A 中动态分配了内存(通过 pa = new int(y);),但是没有定义拷贝构造函数。当使用默认的拷贝构造函数时,只是简单地进行成员变量的值拷贝,对于指针 pa 来说,会导致两个对象的 pa 指针指向同一块内存区域。这样在对象析构时,会对同一块内存进行多次 delete,从而引发运行时错误。

50 引用

我们知道对于一个类型T,可以有这几种引用类型:

  • T&,T的引用,只能绑定到T类型的左值
  • const T&,const T的引用,可以绑定到T的左值和右值,以及const T的左值和右值
  • T&&,T的右值引用,只能绑定到T类型的右值
  • const T&&,一般来说见不到,然而当你对一个const T&使用std::move就能得到这东西了

引用必须在声明的同时进行初始化,所以下面这样的代码应该是大家再熟悉不过的了:

1
2
3
4
5
int num = 0;
const int &a = num;
int &b = num;
int &&c = 100;
const int &d = 100;

新的问题出现了,考虑一下如下代码的运行结果:

1
2
3
int a = 10;
long &b = a;
std::cout << b << std::endl;

一个普通的左值引用不能绑定到一个右值上。因为a是int,b是long,所以a想赋值给b就必须先隐式转换成long。

隐式转换除非是转成引用类型,否则一般都是右值,所以这里报错了。解决办法也很简单:

1
2
long b1 = a;
const long &b2 = a;

要么直接复制构造一个新的long类型变量,值类型的变量可以从右值初始化;要么使用const左值引用,因为它能绑定到右值。

51 强制类型转换

C 风格的强制类型转换
  • 几乎万能:(type)value
  • 调用构造函数式:type(value)
static_cast
  • 给程序员看的(表示某处发生了隐式类型转换)
const_cast
  • 给指针所指向的类型强制去掉 const 性质

    1
    2
    3
    4
    5
    6
    7
    8
    void f(int *p){}
    int main(){
    int a =1;
    const int *p = &a;
    int *q= const cast<int *>(p);
    f(const cast<int *>(p));
    return 0;
    }
dynamic_cast
  • 多态指针的安全转换若指向空间实际所存的对象类型可以隐式转换则转换成功,否则返回 nullptr

  • 注:需要有多态性(虚函数)才能使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class A {
    public:
    virtual int color()=0;
    };
    class B:public A {
    public:
    int color(){ return 0x66ccff;}
    };
    class c : public A{
    public:
    int color(){return 0xee0000;}
    };
    int main(){
    B b;
    A *a = &b;
    B *p1 = dynamic_cast<B*>(a);// succeed
    C *p2 = dynamic cast<c*>(a);// fail
    return 0;
    }
reinterpret_cast
  • 指针之间、指针与其它数据类型之间的强制转换
  • 注:没有 const_cast 的功能,即无法去除 const

52 类中功能函数

原型

image-20250112102801324

注意事项

image-20250112102827119

如果自己定义了有参数版本的构造函数,编译器也不会自动生成默认构造函数了。

53 生命周期

首先区分对象是存放在栈上还是堆上,以及是不是static

栈上:在定义时构造,在作用域结束时析构

堆上:由栈上变量(指针)控制。(new出来的)

static:第一次运行时构造,程序结束时析构

54 str库函数

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
//头文件:#include<string.h>
#include <iostream>
#include <string.h>
#include <string>
#include <cstdio>

using namespace std ;

int main()
{
char a[10] = "123456789" ; //如此定义只能定义10-1个。最后一个放\0 ,最后一个需自己定义。
char b[10] = "qwertyuio" ;

// 1、strlen (求长度)注意求的是实际长度,从第一位开始,一直往后数,直到数到\0(不包括\0)
cout << strlen(a1) ;


// 2、strcpy 、strncpy (复制)
strcpy(a,b) ;//会自动在结尾加上一个\0
strncpy(a,b+3,3) ; //意思是将b字符串的第4-7个值赋给a字符串,不会自动在结尾加上一个\0


// 3、strcmp 、strncmp (比较大小)
cout << strcmp(a,b) ;//大于0是a>b
cout << strncmp(a,b+3,3) ; //比较a和b+3的前3个字符


// 4、strcat 、 strncat (字符串连接)
// 操作逻辑:在第一个字符串中找到\0的位置,从这里开始把第二个字符串中的所有字符一个一个搬过来,最后再加上一个新的\0
strcat(a,b) ;
strncat(a,b+3,3) ; //同上

a[0] = 0;//把a从头截断,这里的0其实就是\0的意思
strcat(a,b);//把b的内容放到a里,这样a就变得和b一样了


// 5、strchr 、 strrchr (查找、反向查找)
//一定、必须要用以下三行代码才能返回该字符的位置。 strrchr同理
char *p ;
p = (char*)(strchr(a,'5')) ;
cout << "要查询的数在数组第" <<(int)(p-a+1) << "位。" << endl ;


//6、特殊查找
string a , b ;
cin >> a >> b ;
// 返回b串中第一个与a串不匹配字符的位置
int found = a.find_first_not_of(b) ;
int found = a.find_first_not_of(b,1) ; //从b串的第二个开始找
int found = a.find_first_not_of(b,1,4) ; //从b串的第二个开始找,到第5个结束
cout << found + 1 ; //返回值需+1.

// 返回b串中第一个与a串匹配字符的位置
int found = a.find_first_of(b) ;

return 0 ;
}

55 枚举类型

枚举类型中,如果没有显式初始化某个枚举常量,那么它的值就是前一个枚举常量的值加1(第一个枚举常量除外,如果第一个枚举常量没有初始化,默认值为0)。当有显式初始化时,后续未显式初始化的枚举常量会在前一个显式初始化或默认初始化的值基础上依次递增。

例如,enum color { Monday=1, Tuesday, Wednesday, Thursday, Friday, Saturday=11, Sunday};中,Sunday是13

56 cin.getline

1
2
3
cin.getline(str,80);//读入一行,以换行符'\n'为结束,不会读入'\n',且会把它从输入流中丢弃,下次读入的时候直接从'\n'的下一个开始,但最多读79个,这其实相当于cin.getline(str,80,'\n');
cin.getline(str,80,'$');//不以'\n'结束,改为'$',同样不会读入且丢弃
//注意读入最多79个是因为读完之后会自动给第80位加上一个'\0'

57 sizeof

基本用法

  • 对数据类型使用:sizeof(数据类型)。例如,sizeof(char) -> 1 , sizeof(int) -> 4 ,sizeof(double) -> 8
  • 对变量使用:sizeof(变量名)

对数组使用

  • 计算数组总大小:返回整个数组占用的内存字节数,等于数组元素个数乘以单个元素所占字节数。例如:
1
2
int arr[5];
cout << sizeof(arr) << endl; // 输出20
  • 计算数组元素个数:结合sizeof对单个元素的结果,可以计算数组元素个数。例如:
1
2
3
int arr[] = {1, 2, 3, 4, 5};
int count = sizeof(arr) / sizeof(arr[0]);
cout << count << endl; // 输出5

对指针使用

  • 在 32 位系统中,指针变量通常占 4 个字节;在 64 位系统中,指针变量通常占 8 个字节,与指针所指向的数据类型无关。例如:
1
2
int* ptr;
cout << sizeof(ptr) << endl; // 在64位系统中输出8

58 类的各类函数

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
#include <iostream>
using namespace std;

class A {
private:
int x;
int sum_count;

public:
A(int a = 1, int c = 0) : x(a), sum_count(c) { }
A(const A& other) {x = a.x,sum_count = a.sum_count;}
//这个叫拷贝构造函数,对应(1)
A& operator=(const A& a){x = a.x,sum_count = a.sum_count;}
//这个叫拷贝赋值运算符重载,可以注意到是返回&,使得可以连续赋值,对应(2)
A(A&& a) noexcept { x = a.x,sum_count = a.sum_count;a.x = 0; a.sum_count = 0;}
//这个叫移动构造函数,把原来的对象指针置空,成员归0,防止析构出错,对应(3)
A& operator=(A&& a){ x = a.x,sum_count = a.sum_count;a.x = 0; a.sum_count = 0;}
//这个叫移动赋值运算符重载,同样是返回&,对应(4)
//这4个可以理解为4象限
};

A a0 = 9;
//此时,会在类A中寻找有没有匹配的构造函数,发现有:则对a0调用构造函数A(9,0)来初始化a0
//如果没有相匹配的构造函数,则编译器报错(无法将int类型9转换为A类)

A a1 = a0;//(1)
A a5(a1);//(1)
//此时会调用拷贝构造(如果没有写过,自动生成),不会调用重载赋值运算符(operator=)

A a2;
a2 = a0;//(2)
//此时会调用拷贝赋值运算符

A a3 = std::move(a0);//(3)
//这里使用move将a0转换为右值,并使用移动构造,减少不必要的拷贝

A a4;
a4 = std::move(a1);//(4)
//这里调用移动赋值运算符

Rmk.在类含有指针变量的时候,必须要重载赋值和拷贝构造函数。

59 noexpect关键字

说明该函数不会抛出异常,可以和同名函数形成重载:

1
2
void func() {普通版本}
void func() noexcept {noexcept版本}

移动构造函数和移动赋值运算符中,通常会使用noexcept说明符。这是因为移动操作通常被期望是无异常的

60 解引用

对于一维数组,解引用相当于获取到了当前指针指向的值,如:

1
2
int a[10];
*(a + 9) = 1;

这也常被用于在函数中使用指针作为参数直接修改对象的值,避免拷贝,作用和引用传递类似,如指针版的swap函数。

对于二维数组,解引用相当于获取到了下一层的指针(有两层皮,一次解引用只蜕一层,两次才能获取到对应的值),如:

1
2
int a[10][5];
*((*(a + 9)) + 4) = 1;//这是二维数组的最后一个元素

61 类的类型转换函数重载

  • 类型转换函数的声明形式为 operator 目标类型(),例如目标类型是 double,就应该是 operator double()

  • 它没有参数,也不需要指定返回类型(因为返回类型就是目标类型 double),函数体中实现将 Rational 对象转换为 double 类型的具体逻辑。

    例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Rational {
// 类的其他成员和实现
public:
operator double() {
// 这里实现将 Rational 对象转换为 double 的具体计算,例如返回分子除以分母的结果
return static_cast<double> (numerator) / denominator;
// 这里使用static_cast<double>,是调用了c++的显示类型转换,其实c++知道你需要返回double的时候,会自动帮你改为double类型的,(例如这里)。也可以使用(double)代替以保证精度。
}
private:
int numerator;
int denominator;
};

Rational r(3, 2);
double d = r;
// 这里会自动调用operator double() 将 r 转换为 double 类型并赋值给 d(隐式类型转换)

double sum = 1.0 + static_cast<double>(r); // 显式类型转换,使用 static_cast(同样也会找到我们所定义的那个函数来转换)

if(double)r + 1 == 3)...//注意这样写是错的,没有这样的用法

62 命名规范

  • 开头字符限制:标识符必须以字母(a - zA - Z)或下划线(_)开头,不能以数字或其他特殊字符开头。如 int 3abc; 是错误的,而 int _abc;int abc_3; 是正确的。
  • 字符组成限制:除开头字符外,后续字符可以是字母、数字(0 - 9)或下划线。不能包含空格、标点符号等其他特殊字符,如 int my-var; 是错误的,正确的是 int my_var;

63 模板的友元

类模板可以有友元函数或者友元类,分为两种:

1.类模板或者函数模板的特定实例
1
2
3
4
5
6
7
template <class T> class B;
template <class T> void f(const T &);
template <class type>
class A{
friend class B <int>;//只有int实例化的B才是友元
friend void f(cosnt double &);//只有double实例化的f才是友元
};
2.约束友元
1
2
3
4
5
6
7
template <class T> class B;
template <class T> void f(const T &);
template <class type>
class A{
friend class B <type>;//只有相同实例化的B才是友元
friend void f(cosnt type &);//只有相同实例化的f才是友元
};

64 自增自减运算符的重载

  • 后置++的函数声明形式friend <类名> operator++(<类名>&, int);,其中第一个参数是要进行自增操作的对象的引用,第二个参数int是一个用于区分前置和后置自增的占位参数,没有实际的参数名。
  • 前置++的函数声明形式friend <类名>& 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
class MyClass {
public:
// 声明后置自增运算符的友元函数
friend MyClass operator++(MyClass&, int);//多一个int是为了识别是后置,仅此而已
// 声明前置自增运算符的友元函数,注意前置返回的是&
friend MyClass& operator++(MyClass&);

int getValue() const { return value; }
private:
int value;
};

// 后置自增运算符的友元函数定义
MyClass operator++(MyClass& obj, int) {
// 先保存当前对象的副本
MyClass temp = obj;
// 对对象进行自增操作
obj.value++;
// 返回自增前的对象副本
return temp;
}

// 前置自增运算符的友元函数定义
MyClass& operator++(MyClass& obj) {
// 对对象进行自增操作
obj.value++;
// 返回自增后的对象引用
return obj;
}
  • 调用方式:当使用后置自增运算符时,如obj++,编译器会将其解释为operator++(obj, 0)。使用前置自增运算符时,如++obj,编译器会直接调用operator++(obj)

65 操作符的重载

如+,=等操作符,重载有严格的规定,操作数的优先级、结合性、操作数个数都不能改变。

66 数组的初始化

会产生随机值的情况:

1.局部数组未初始化
1
2
int a[5];//这时会产生随机值
int a[5] = {1};//虽然只赋了一个值,但是由于有初始化,后面的会变成0
2.动态分配的数组
1
int *a = new a[5];//产生随机值

67 源文件的连接

在一个源文件中使用一个函数的时候,如果这个函数定义在另一个源文件中,不可以直接使用,需要在这个源文件中进行声明,此时编译器才会去别的文件中寻找定义。

68 对指针的引用

1. char*&

  • char*& 是一个指向 char 指针的引用。

  • 可以将其分解为 char*(指向 char 的指针)和 &(引用)两部分。它的作用是创建一个对 char 指针的引用,允许你通过这个引用修改所指向的指针本身。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    char str[] = "Hello";
    char* ptr = str;
    // 定义一个指向 char 指针的引用
    char*& a = ptr;
    cout << a << endl;
    // 修改引用所指向的指针
    a = new char[10];
    strcpy(a, "World");
    cout << a << endl;
  • 在这个例子中,ptrRef 是对 ptr 的引用。当修改 ptrRef 时,实际上修改的是 ptr

2. char&* 不合法

69 链表

带头节点的链表,头结点是不参与实际内容组成的,只是为了做一个节点,没有任何意义。在排序的时候也不考虑它。

70 类模板(教材)

由于类模板的成员函数可能用到模板参数,所以类模板的所有成员函数全部为函数模板,如果在类内定义不需要再加模板声明,如果是类外,需要一定的修改。

类模板只有在被调用的时候才会实例化,所以编译器不会检查具体的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
class A {
public:
T a;
void aaa() ;
void mod(){ a %= 2}//这里虽然对于T是double等不能取模情形是错误的,但此时不会实例化,不会报错

};
template<class T>
void A<T>::aaa() {return;}

A<int> a(5);
A<double> b(3);//这里不会报错,因为是隐式实例化,如果在main之前加上显式实例化的声明template class A <double>,就会报错了
a.mod();
b.mod();//这里才会报错(使用的mod的时候才会实例化)

类模板可以特化,定义格式如下:

1
2
3
4
5
6
7
8
9
10
11
template<> 
class A<double> {...};

A<double> a;//此时就会调用特化版本

//也可以特化部分参数,如:
template<class T,class S>
class A{};

template<class S>
class A<int,S>{};//特化T为int

71 一个类的例子

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
template<class T>
class Array{
T x;
T *str;
public:
Array(T y,T* z):x(y){
str = new T [x];
for(int i = 0 ; i < x;i++){
str[i] = z[i];
}
}
Array(const Array& y){
x = y.x;
str = new T[x];
for(int i = 0 ; i < x;i++){
str[i] = y.str[i];
}
}
Array& operator=(const Array& a){
if(this == &a){
return *this;
}
delete []str;
str = new T[x];
for(int i = 0 ; i < x;i++){
str[i] = a.str[i];
}
return *this;
}
~Array(){
delete [] str;
}
T& operator[](int i){
if(i < x && i >= 0){
return str[i];
}
throw out_of_range("Array index out of range");
}
int size(){
return x;
}

}

72 数组与指针

image-20250114162609734

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
char array [5] = "abc";
cout << array << endl;//输出abc
cout << &array << endl;//输出整个数组的地址,也就是第一个元素的地址
cout << array + 1 << endl;//输出bc
cout << &array + 1 << endl;//+1向后移动一整个数组(大小为5),输出&array+5的结果
cout << *array << endl;//输出a(解引用)
cout << *array + 1 << endl;//输出98(a+1)
cout << &array[0] + 1 << endl;//输出bc,+1向后移动一整个char(大小为1)
return 0;
}
Comments