——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.优先级
==!最高,(),算术运算符,关系表达式(>,!=等),&&,||,赋值运算符(=,+=,*=等)最低==
2.
大多数运算符都可以重载,但有一些运算符是不能重载的:
- 成员访问运算符(
.
) - 成员指针访问运算符(
.\*
和->\*
) - 作用域解析运算符(
::
) - 条件运算符(
?:
) - sizeof 运算符(
sizeof
)
3.
C++提供的运算符按照运算数的个数分为两种:一元运算符和二元运算符(X) 还有三元运算符:?:表达式
3 构造函数
使用类的时候,在A p;或者A p = new A(..)的时候会调用构造函数,但是在A *p1;的时候是不会的,只是建了一个指针,没有新建类对象,在之后p1 = new A(…)的时候才会。
判断:对象作为实参时系统将自动调用拷贝构造函数(X)
值传递会调用拷贝构造函数,传递引用/指针的时候不会调用
后两种直接修改原始对象,拷贝构造不会修改原始对象(修改的是一个副本)
三种特殊情况:
1. 基类构造函数需要参数,派生类本身不需要构造函数时
- 派生类必须定义构造函数:当基类的构造函数需要参数时,派生类必须定义自己的构造函数,即便派生类自身不需要进行额外的初始化操作。
1 | class Base { |
2. 基类使用缺省或不带参数的构造函数
- 派生类构造函数初始化列表可略去基类构造函数名(参数表):
1 | class Base { |
3. 省略派生类构造函数的情况
1 | class Base { |
4 析构函数
A &b = a;
则b是a的引用,在a析构的时候b也就没了。所以在程序结束的时候,只会调用a的析构函数。
A a或者A *a = new A等都是需要析构的,无论是否是new出来的
构造函数和析构函数都没有返回类型,不等价于返回void!这是两个完全不同的概念。
构造函数可以重载,析构函数不能
析构函数没有参数
先构造,后析构;后构造,先析构!构造先基类后派生类,析构先派生类后基类
1 | class A{ |
全局变量和静态局部变量是同等地位,谁先析构看谁后构造
1 | class 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 | int a[3]={1,3 ,5},*p; |
2.
指针在没有赋初值的时候不可以直接使用,例如,
1 | studentT student1, *sp; |
这三种使用都是错的
9 虚函数的使用
1.
1 | class Base{ |
2.
1 | class 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 | classA { |
4.教材中的虚函数内容(使用方法以及override,final,虚析构函数,纯虚函数)
1 | class A{ |
override的使用
显式指定覆盖,让编译器帮你检查一遍覆盖的函数原型是否和虚函数一致,如果不一致则报错
1
2
3
4
5
6
7
8
9
10
11class 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
8class 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
12class 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 | void f(A&x,const A&y){...} |
11 坑
1.=和==:
如果题目中的判断条件是“=”,仔细看!可能不是想象的那样
2.保护符
程序改错的时候,要注意:如果有多个cpp包含了一个头文件,要注意这个头文件有没有保护符
3.vector迭代器
vector在扩容(比如push_back)的时候,所用的迭代器都会失效,所以不能
1 | vector<int> a; |
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 | class Array{ |
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 | friend ostream& operator<<(ostream &os, const foo &obj) //返回的应该是原样的os,是引用,要用& |
注意格式(第一排)
可以写在类里(但一般写在类外),记得要用友元函数。
operator@运算符重载(=,.,[],()必须重载为成员函数,流插入符号为全局函数(友元)
原因:如果将其重载为类的成员函数,那么调用方式就会变成obj << cout
,这与我们通常使用的cout << obj
的习惯不符。
17 异常
1 | for (int i = 1; i < 6; ++i) |
在这段代码中,如果在f(3)的时候抛出异常,则会进入catch块,但是不会出for(catch结束之后,进行下一次循环)
1 | setTry newOne[3]; |
但是,在这个里,由于catch是不在for里面的,所以catch之后直接走人,不再继续for
18 模版
在C++中,模板包括函数模板与类模板
类模板的成员函数可以是普通函数,不可以是函数模板
函数模版的格式:
template < 模板参数列表 >返回值函数名 (函数参数列表)
eg.template <class T> T square(T x) { return x * x; }
使用的时候直接 square(100)即可
类模板的非类型模板参数要求在编译期就能确定其具体的值,必须是常量表达式,例如:
1 | template <class T, int L> |
19 静态成员
静态数据成员是类的所有对象共享的成员,它不依赖于任何对象而存在,在程序开始执行时就会被分配内存空间,所以在建立对象前,就可以为静态数据成员赋值,比如可以通过类名加作用域运算符::
来对静态数据成员进行赋值。
eg.
1 | class A{ |
静态成员函数既可以在类外定义,也可以在类内定义,在类内定义时,隐式地内联(只有static const int这种整型静态常量可以)。不可以放在类的构造函数里初始化。(结合46看)
this
指针指向的是调用成员函数的对象,而静态成员函数不属于任何一个具体的对象,是属于整个类的,所以在静态成员函数中不能使用this
指针.
在使用静态成员的时候,既可以使用A::a,也可以使用obj.a
静态成员函数一样,
1 | class 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 | for(int i = 0 ; i< n ; i++){ |
27 const
在一个函数中,如果调用了自己建的类,如void func(const A& a){}
,const说明不会更改a的内容,那么就只能调用A类里面承诺不会更改成员的函数,如void get() const {...}
,这个const的位置,表示不会修改成员变量,注意const不是加在最前面的,加在最前面,如const int get(){...}
表示的是返回值不能修改(不会报错,但基本不会这么用)
const 数据成员只能初始化列表,不能赋值
对const对象只能调用const成员函数,且必须初始化
举例:
1 | class MyClass { |
const指针:
区分方法:去掉类名之后看const的右边有没有*,如果有,说明不能改变的是值,没有,说明不能改变的是指针本身(指向性)
1. 不许改变指针本身(己值和他址):int* const p;
- 含义:这种声明方式表示
p
是一个常量指针。一旦p
被初始化指向某个地址,就不能再让它指向其他地址了。但是,通过p
可以修改指向的对象的值。 - 示例:
1 | int a = 5; |
2. 不许改变指针所指的对象(他值):const int *p
或 int const *p;
- 含义:这两种声明方式表示
p
是一个指向常量的指针。也就是说,不能通过p
去修改它所指向的对象的值,但p
本身可以被重新赋值,指向其他的地址。 - 示例:
1 | int a = 5; |
辨析:
1 | const int a = 10 , *p = &a;//合法,定义一个常量a之后,用一个const int *(不会修改指向对象的值的指针)来指向a |
28 类型转换
一般来说,类型转换是朝着能够容纳更多信息或者更通用的类型方向进行的。int
类型比 char
类型能表示更大范围的值,所以在这种算术运算中,char
会向 int
转换,而不是反过来将 int
转换为 char
所以,在auto a = ‘a’ + 0x10
的时候,a会被转换为int类型,而不是把0x10转换为char类型
long double
>double
>float
long long
>long
>int
>short
>char
- 无符号类型>有符号类型
29 decltype关键字
decltype用于推导一个变量的类型,使用例子:int x; decltype (x) y; //此时相当于构造了一个int类型的y
30 字符串和字符数组
1 | char str1 [] = {‘h’, ’1’, ‘2’, ‘a’, ‘b’}, str2[4]; |
在这里,由于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 | constexpr int a = 10; |
constexpr
函数是指能用于常量表达式的函数,其函数体在编译阶段就可以被求值。
函数的返回值类型必须是字面值类型,函数体中只能有一条return
语句,且return
返回的表达式必须是常量表达式或可在编译时求值的表达式。例如:
1 | constexpr int add(int a, int b) { |
32 拷贝构造和移动构造
拷贝构造:A(const A & other);
常量左值
移动构造:A(A && other);
右值
在对象之间互相赋值时会自动调用拷贝构造函数 MyClass(const MyClass& other);
(x)
会调用赋值运算符重载函数:MyClass& operator=(const MyClass& other);
对象之间的赋值与拷贝构造无关,注意避免混淆
1 | Rational 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 | int &f( int &k1 ) |
注意这段代码中的返回部分,要返回一个引用!
++k2
:前置自增运算符++k2
会先将k2
的值加 1,然后返回k2
本身的引用。k2++
:后置自增运算符k2++
会先返回k2
的值,此时返回的是一个临时对象,它是k2
的原始值的副本,然后再将k2
的值加 1。当函数返回这个临时对象的引用时,由于临时对象在表达式结束后就会被销毁,导致返回的引用指向了一个已经不存在的对象,这就产生了悬空引用,会引发未定义行为。
35 缺省值
People(char *n = nullptr, int day) //这个缺省值不符合语法规范!People(1)是不可以的
函数参数中一个参数有缺省值,后面参数都必须有缺省值,否则无法利用到这个缺省值(缺省值必须全部后置,把所有没有缺省值的参数尽量往前放)
36 模版形参
1 | template <int LEN> |
37 将派生类对象隐式转换为基类对象(多态,教材)
1 | class A{ |
38 常引用
const A &k2 = k1;
这一步相当于是把k1此时的样子记录下来了,之后无论k1怎么变化,k2始终是此时的样子
39 文件输入输出以及命令行
int argc, char *argv[]
用于命令行输入,argc是参数个数,argv是每个参数
注意头文件:
1 |
|
文件输入输出例子:
1 | class encrypt { |
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
),用于设置文件写入指针的位置。
tellg
和tellp
分别用于获取文件读取指针和写入指针的当前位置,和上面两个相对应。
例子:
1 | in.seekg( n );//绝对位置 |
42 手写链表
1 | 学!一定要手写几遍。 |
43 cstring
44 swap的两种方式
指针参数方式
1 | void swap(int *m, int *n) { |
调用:swap(&x, &y)
注意:
1 | void myfun(int*q , int &n) { |
引用参数方式
1 | void swap(int &m, int &n) { |
调用:swap(x, y)
需要注意的是,函数使用引用参数时,实参必须是变量,而不能是一个表达式。
45 返回指针或引用
当函数返回值是指针时返回地址对应的变量不能是一个局部变量。
函数的返回值也可以是一个引用。它表示函数的返回值是函数内某个变量(不能是局部变量)的引用。
返回引用之后,可以这样:
1 | int a[5] = {1,2,3,4,5}; |
46 类的声明与赋初值
类的定义只是声明了成员,而没有定义成员,所以不能给数据成员赋初值!
1 | class 类名 { |
47 explicit关键字
1. 作用
- 防止隐式类型转换:当一个构造函数被声明为
explicit
时,它不能用于隐式地将其他类型转换为该类的对象。这可以避免一些意外的、可能不符合程序员意图的类型转换,使代码更加清晰和安全。
2. 示例
1 | class MyClass { |
如果将构造函数声明为explicit
1 | class MyClass { |
此时,MyClass obj2 = 10;
和func(20);
这样的代码将无法编译通过,因为explicit
禁止了这种隐式类型转换,必须显式地使用MyClass obj(10);
这样的方式来创建对象或调用函数。
48 词汇
1. hex16
、oct8
、dec10
- 含义
hex16
:表示十六进制格式,oct8
:表示八进制格式,dec10
:表示十进制格式
- 示例:
1 | int num = 255; |
2. setbase()
- 含义:用于设置输出整数的进制基数。
- 示例:
1 | int num = 100; |
3. fixed
、setprecision()
、.precision
、scientific
- 含义
fixed
:用于设置浮点数以定点数形式输出,即显示固定小数位数的浮点数。setprecision()
:用于设置浮点数输出的精度(即小数位数),需要包含<iomanip>
头文件。.precision
:是ostream
类的成员函数,也用于设置浮点数输出的精度。scientific
:用于设置浮点数以科学计数法形式输出。
- 示例:
1 | double num = 123.456789; |
上述代码中,fixed
结合setprecision(2)
将num
以定点数形式输出且保留两位小数,scientific
将num
以科学计数法形式输出。
4. width()
、setw
、setfill
- 含义
- 后两个需要
#include<iomanip>
width()
:是ostream
类的成员函数,用于设置输出字段的宽度。setw
:是<iomanip>
头文件中的函数,功能与width()
类似,用于设置输出字段的宽度。setfill
:用于设置填充字符,当输出字段宽度大于要输出的内容宽度时,用设置的填充字符填充剩余宽度。
- 示例:
cout << setw(10) << setfill('*') << 123 << endl;
在这个例子中,setw(10)
设置输出宽度为10
,setfill('*')
设置填充字符为*
,输出123
时会在前面填充*
使总宽度达到10
。
49 有delete要有拷贝控制
1 | class A { |
在代码中,类 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 | int num = 0; |
新的问题出现了,考虑一下如下代码的运行结果:
1 | int a = 10; |
一个普通的左值引用不能绑定到一个右值上。因为a是int,b是long,所以a想赋值给b就必须先隐式转换成long。
隐式转换除非是转成引用类型,否则一般都是右值,所以这里报错了。解决办法也很简单:
1 | long b1 = a; |
要么直接复制构造一个新的long类型变量,值类型的变量可以从右值初始化;要么使用const左值引用,因为它能绑定到右值。
51 强制类型转换
C 风格的强制类型转换
- 几乎万能:(type)value
- 调用构造函数式:type(value)
static_cast
- 给程序员看的(表示某处发生了隐式类型转换)
const_cast
给指针所指向的类型强制去掉 const 性质
1
2
3
4
5
6
7
8void 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
19class 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 类中功能函数
原型
注意事项
如果自己定义了有参数版本的构造函数,编译器也不会自动生成默认构造函数了。
53 生命周期
首先区分对象是存放在栈上还是堆上,以及是不是static
栈上:在定义时构造,在作用域结束时析构
堆上:由栈上变量(指针)控制。(new出来的)
static:第一次运行时构造,程序结束时析构
54 str库函数
1 | //头文件:#include<string.h> |
55 枚举类型
枚举类型中,如果没有显式初始化某个枚举常量,那么它的值就是前一个枚举常量的值加1
(第一个枚举常量除外,如果第一个枚举常量没有初始化,默认值为0
)。当有显式初始化时,后续未显式初始化的枚举常量会在前一个显式初始化或默认初始化的值基础上依次递增。
例如,enum color { Monday=1, Tuesday, Wednesday, Thursday, Friday, Saturday=11, Sunday};
中,Sunday是13
56 cin.getline
1 | cin.getline(str,80);//读入一行,以换行符'\n'为结束,不会读入'\n',且会把它从输入流中丢弃,下次读入的时候直接从'\n'的下一个开始,但最多读79个,这其实相当于cin.getline(str,80,'\n'); |
57 sizeof
基本用法
- 对数据类型使用:
sizeof(数据类型)
。例如,sizeof(char) -> 1 , sizeof(int) -> 4 ,sizeof(double) -> 8
- 对变量使用:
sizeof(变量名)
对数组使用
- 计算数组总大小:返回整个数组占用的内存字节数,等于数组元素个数乘以单个元素所占字节数。例如:
1 | int arr[5]; |
- 计算数组元素个数:结合
sizeof
对单个元素的结果,可以计算数组元素个数。例如:
1 | int arr[] = {1, 2, 3, 4, 5}; |
对指针使用
- 在 32 位系统中,指针变量通常占 4 个字节;在 64 位系统中,指针变量通常占 8 个字节,与指针所指向的数据类型无关。例如:
1 | int* ptr; |
58 类的各类函数
1 |
|
Rmk.在类含有指针变量的时候,必须要重载赋值和拷贝构造函数。
59 noexpect关键字
说明该函数不会抛出异常,可以和同名函数形成重载:
1 | void func() {普通版本} |
在移动构造函数和移动赋值运算符中,通常会使用noexcept
说明符。这是因为移动操作通常被期望是无异常的
60 解引用
对于一维数组,解引用相当于获取到了当前指针指向的值,如:
1 | int a[10]; |
这也常被用于在函数中使用指针作为参数直接修改对象的值,避免拷贝,作用和引用传递类似,如指针版的swap函数。
对于二维数组,解引用相当于获取到了下一层的指针(有两层皮,一次解引用只蜕一层,两次才能获取到对应的值),如:
1 | int a[10][5]; |
61 类的类型转换函数重载
类型转换函数的声明形式为
operator 目标类型()
,例如目标类型是double
,就应该是operator double()
。它没有参数,也不需要指定返回类型(因为返回类型就是目标类型
double
),函数体中实现将Rational
对象转换为double
类型的具体逻辑。例如:
1 | class Rational { |
62 命名规范
- 开头字符限制:标识符必须以字母(
a
-z
、A
-Z
)或下划线(_
)开头,不能以数字或其他特殊字符开头。如int 3abc;
是错误的,而int _abc;
和int abc_3;
是正确的。 - 字符组成限制:除开头字符外,后续字符可以是字母、数字(
0
-9
)或下划线。不能包含空格、标点符号等其他特殊字符,如int my-var;
是错误的,正确的是int my_var;
。
63 模板的友元
类模板可以有友元函数或者友元类,分为两种:
1.类模板或者函数模板的特定实例
1 | template <class T> class B; |
2.约束友元
1 | template <class T> class B; |
64 自增自减运算符的重载
- 后置++的函数声明形式:
friend <类名> operator++(<类名>&, int);
,其中第一个参数是要进行自增操作的对象的引用,第二个参数int
是一个用于区分前置和后置自增的占位参数,没有实际的参数名。 - 前置++的函数声明形式:
friend <类名>& operator++(<类名>&);
,参数是要进行自增操作的对象的引用,返回值也是对象的引用,以便支持连续操作。 - 实现示例
1 | class MyClass { |
- 调用方式:当使用后置自增运算符时,如
obj++
,编译器会将其解释为operator++(obj, 0)
。使用前置自增运算符时,如++obj
,编译器会直接调用operator++(obj)
。
65 操作符的重载
如+,=等操作符,重载有严格的规定,操作数的优先级、结合性、操作数个数都不能改变。
66 数组的初始化
会产生随机值的情况:
1.局部数组未初始化
1 | int a[5];//这时会产生随机值 |
2.动态分配的数组
1 | int *a = new a[5];//产生随机值 |
67 源文件的连接
在一个源文件中使用一个函数的时候,如果这个函数定义在另一个源文件中,不可以直接使用,需要在这个源文件中进行声明,此时编译器才会去别的文件中寻找定义。
68 对指针的引用
1. char*&
char*&
是一个指向char
指针的引用。可以将其分解为
char*
(指向char
的指针)和&
(引用)两部分。它的作用是创建一个对char
指针的引用,允许你通过这个引用修改所指向的指针本身。1
2
3
4
5
6
7
8
9char 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 | template<class T> |
类模板可以特化,定义格式如下:
1 | template<> |
71 一个类的例子
1 | template<class T> |
72 数组与指针
1 | int main() |