C++学习
C++学习
快捷键:(减少缩进,c+[)
typedef
1 | typedef char* String; // 'String' 是 'char*' (指向char的指针) 的别名 |
typedefvs #define:
typedef是编译器处理的,它进行的是类型别名定义。#define是预处理器处理的,它进行的是==文本替换。==- 在定义类型别名时,
typedef更安全、更符合语言规范,作用域规则也符合C/C++的变量作用域(如可以在函数内部定义)。而#define是全局的文本替换,容易出错(尤其是在指针类型定义时)。优先使用typedef来定义类型别名。
signed 和unsigned
对于有符号类型(默认是 signed,但通常省略关键字),最高位(MSB)用作符号位(0表示正,1表示负),其余位表示数值。对于无符号类型,所有位都用于表示数值。`
signed char
:范围是-128到127。01111111
= 12700000000
= 010000000
= -128 (二进制补码表示)==short,int,long,long long默认是,char类型在C/C++标准中既不是明确规定为signed char也不是unsigned char`。它的符号性是由编译器和目标平台实现定义的。==负数部分用补码表示:==绝对值取反加一==
- 取绝对值的二进制:先得到该负数的绝对值的二进制形式(正数表示)。
- 取反(invert):将所有位翻转(0变1,1变0)。
- 加1:对取反后的结果加1。
例子1:-1的表示
- 绝对值是1:二进制
00000001(8位表示)。 - 取反:翻转所有位 →
11111110。 - 加1:
11111110 + 1 = 11111111。 - 所以,-1 的补码是
11111111。
unsigned char:范围是0到255。11111111= 25500000000= 010000000= 128 (所有位都表示数值)==只能表示0和正整数==
重要注意事项:
溢出行为:
unsigned类型的算术运算是模运算。如果结果超出最大值,它会“回绕”到最小值(0)并继续向上。例如:1
2unsigned int u = 5;
u = u - 10; // 结果是一个非常大的正数(假设int是32位,结果是4294967291)这通常不是期望的行为,需要程序员小心处理边界条件。
与有符号数混合运算: 当
unsigned类型和signed类型一起运算时,C/C++ 有一套复杂的类型转换规则(称为整数提升和寻常算术转换)。通常,signed类型会被隐式转换为unsigned类型,这可能导致意外的巨大正数结果:1
2
3
4
5int i = -5;
unsigned int u = 10;
if (i < u) { // 这里 i (-5) 会被转换为 unsigned int (一个很大的正数)
// 这个条件为 false! 因为 (非常大的数 > 10)
}==强烈建议避免混合使用
signed和unsigned类型进行比较或运算。== 如果必须混合,请显式进行类型转换并理解其含义。循环计数器: 使用
unsigned类型作为循环计数器(尤其是递减循环)时要格外小心:1
2
3
4for (unsigned int i = 10; i >= 0; i--) {
// ... 这将是一个无限循环!
// 当 i 为 0 时,i-- 会变成最大值(如4294967295),仍然 >= 0
}安全的做法是:
1
2
3
4
5
6
7
8for (unsigned int i = 10; i > 0; i--) { // 在 i=1 时停止
// ...
}
// 或者如果需要处理 i=0 的情况:
unsigned int i = 10;
do {
// ... 处理 i
} while (i-- > 0); // 先处理,再减,并在减到0后停止
bool
简单来说,bool就是一个开关,只有开(true)和关(false)两个状态
1 | int age = 20; |
1 | if (isRaining) { // 如果 isRaining 为 true (下雨了) |
形参
默认参数必须从 “最右边” 开始连续指定(即无默认值的参数不能在有默认值的参数右边)。该原型中,int y(无默认值)在int x=9(有默认值)右边,违反规则。
函数重载
函数重载的要求是:同一作用域下,函数名相同,但 “参数列表不同”(参数个数不同、或至少一个参数的类型不同)。这两个函数:
- 函数名相同(都是
Add); - 参数列表完全相同(都是两个
int类型的参数,形参名a/b和x/y不同不影响,形参名只是 “占位符”);
函数重载(overload)允许有多个同名函数,但它们的参数列表必须不同(参数个数、类型或顺序不同)
==函数重载只基于参数列表(参数个数、类型或顺序)==,返回值不同不能用于重载。如果两个函数参数完全相同,仅返回值不同,编译器无法区分它们,会导致编译错误
重载函数可以有默认参数(如 void func(int a, int b=10);),但如果有==多个重载版本,调用时可能引起二义性==(ambiguity)。例如:
void func(int a);和void func(int a, int b=5);- 调用
func(10)时,编译器不知道选择哪个版本(第一个需要1个参数,第二个可以接受1个参数)。所以,虽然允许默认参数,但要避免二义性。
在类的成员函数中,const关键字可以区分重载函数。const成员函数用于常量对象,而非 const成员函数用于非常量对象。
1 | class MyClass { |
==具体怎么区分==
命名空间
cout是命名空间std中定义的一个全局对象,需要包含头文件。
指针
const在*左边,内容不可修改,右边,内容可修改
把*xx看作具体内容
const char& p:是常量引用,不是指针。
char* const p是指针常量,必须定义时初始化(p=&a不能延后)。
const double* point是指向常量的指针,定义时可以不初始化(指针本身不是常量)
在C++中,函数不能直接返回数组类型(如 int[] func()是无效语法)。如果函数需要“返回数组”,通常返回指针(如 int* func()),该指针指向数组首元素的地址。
引用
引用(&)必须在==定义时初始化(如const char& p=a;),不能先声明后赋值。==
类型转换方式
y = static_cast
函数形式的强制转换(double(x))
y = double(x);
函数参数规则:必须从左向右连续提供参数,不能跳过中间参数(即使有默认值)
这个和函数重载区别在于?
1 |
|
C++函数只能返回0或1个值
为什么不对:char* p=&’C’;
double* const pt = new double(5.5);
强转
y = int(x);C.y = (int)x;D.y = static_cast
类

构造函数是创建对象时自动调用的特殊函数。它的名字必须和类名一模一样,这是C++的硬性规定
==(类:定义好类型,类似结构体)==
1 | class Box |
1 | Box Box1; // 声明 Box1,类型为 Box |
==声明类的对象,就像声明基本类型的变量一样。==
==类与对象和关系与数据类型和变量的关系相似==

返回区别:
class Time {
public:
// 声明部分 - 都要写返回类型
Time(int d, int h, int m); // 构造函数特殊,没有返回类型
Time Sum(const Time & t); // 返回Time对象
void Show() const; // 不返回值,用void
};
// 定义部分 - 也要写返回类型
Time::Time(int d, int h, int m) { … } // 构造函数没有返回类型
Time Time::Sum(const Time & t) { … } // 返回Time对象
void Time::Show() const { … } // 不返回值,用void
隐含的输入:==成员函数==可以==访====问类的私有成员real和image不需要显式参数==:因为函数内部可以直接使用当前对象的real和image
友元函数
友元函数不是类的成员函数。它只是一个普通的外部函数,但被类“邀请”为朋友,所以能访问类的私有数据。==成员函数是定义在类内部的,而友元函数是外部的。==
如果类S是类A的友元类,那么S的所有成员函数都能访问A的私有和保护成员。就像S是A的“全家朋友”,S的所有函数都有权限。
- 友元函数可访问类的所有成员(包括私有和保护)。
- 友元函数的形参可以是类自身(如
void fun(A obj))。 - 若A是B的友元,则A可访问B的私有成员(而非B访问A)。
- 友元关系不对称(A是B的友元 ≠ B是A的友元)。
- 友元关系不传递(A→B→C ≠ A→C)。
静态成员(如 static int count;)属于类本身,而不是单个对象。所有对象共享同一个副本。例如,如果类有10个对象,静态成员 count只有一个,大家共用。
你好!很高兴能帮助你学习 C++。友元函数(friend function)是一个非常有趣且有用的概念。对于初学者来说,我们可以通过一个简单的比喻来理解它。
成员函数属于类,需要用类名::指明它属于哪个类
友元函数(不需要类名::前缀)
1 | // 类内声明 |
友元函数是什么?一个生活中的比喻
想象一下,你的房间是一个类(class),房间里的东西就是类的成员(members)。有些东西你放在客厅,大家都能看到和使用,这些是公有成员(public)。而你的日记本放在抽屉里,这是私有成员(private),通常只有你自己(类的成员函数)才能翻看。
现在,你有一个非常要好的朋友。虽然他不是你的家庭成员,但你非常信任他,所以你允许他进入你的房间,甚至翻看你的日记。在 C++ 中,这个“好朋友”就是友元函数。
核心思想: 友元函数本身不是类的成员函数,但它被类授予了访问该类所有私有和保护成员的特权。
为什么要使用友元函数?
你可能会问,我们为什么要破坏封装性,让一个“外人”来访问私有成员呢?主要有以下几种情况:
- 运算符重载: 当我们重载某些运算符(如
<<用于输出)时,它们的左操作数通常是ostream对象(如cout),而不是我们自己定义的类的对象。这时,将这个运算符重载函数作为类的友元,就可以方便地访问类的私有成员。 - 类之间共享数据: 当你需要一个函数来操作两个或多个不同类的私有数据时,将这个函数声明为这两个类的友元会非常方便。
- 提高效率: 在某些特定情况下,通过友元函数直接访问数据成员可能比通过公有成员函数调用更高效,减少了函数调用的开销。
如何定义和使用友元函数?
语法其实很简单,只需要在类的定义中,在你希望成为友元的那个函数声明前加上 friend 关键字即可。
1. 普通函数作为友元
这是最常见的一种形式。我们来看一个具体的例子:
1 |
|
代码讲解:
- 在
Box类中,我们声明了friend void printWidth(const Box& box);。这就告诉编译器,printWidth这个函数是我们的朋友,请给它访问私有成员的权限。 printWidth函数是在类外部定义的,它是一个普通的全局函数,不属于Box类。- 在
printWidth函数内部,我们可以直接通过box.width访问Box对象的私有成员。 - 在
main函数中,我们像调用其他任何普通函数一样调用printWidth。
2. 其他类的成员函数作为友元
有时候,我们可能需要让类 A 的某个成员函数去访问类 B 的私有成员。这时可以把类 A 的那个成员函数声明为类 B 的友元。
友元函数的特点总结
- 不是成员函数: 友元函数不属于任何类,它的定义在类外部,因此它没有
this指针。 - 破坏封装性: 这是友元函数最主要的缺点。它打破了类的封装原则,使得类的私有数据暴露给了外部函数,因此应该谨慎使用。
- 单向性: 友元关系是单向的。如果类 A 将一个函数声明为友元,不代表这个函数也能访问其他类的私有成员,也不代表类 A 可以访问那个函数所在类的私有成员。
- 不具传递性: 如果类 A 是类 B 的友元,类 B 是类 C 的友元,这并不能推导出类 A 是类 C 的友元。
何时应该(或不应该)使用友元?
- 应该使用: 当你需要实现一些与类紧密相关,但又不适合作为成员函数的功能时,特别是运算符重载。
- 谨慎使用: 不要滥用友元。在授予友元关系之前,请仔细考虑是否真的有必要。通常,我们可以通过定义更多的公有成员函数来满足需求,虽然有时会显得繁琐一些。
拷贝构造函数
拷贝构造函数用于创建一个新对象,并用另一个同类型对象来初始化它。例如,A obj2 = obj1;会调用拷贝构造函数。
1 | class Book { /*...*/ }; |
对已有对象的修改(改名),调用的是赋值运算符函数 operator=,而不是拷贝构造函数。
1 | Book bookA("C++ Primer"); |
==区别在于新的对象是否已经存在==
构造函数,析构函数
能否重载
- 构造函数可以被重载,比如一个类可以有带参数的构造函数和不带参数的构造函数。
- 析构函数是对象销毁时调用的函数,每个类只能有一个析构函数,名字固定(类名前加
~),不能重载。因为析构函数没有参数,所以不能有多个版本。
调用
- 当对象数组(如
A arr[5];)的生命结束时,C++会自动为数组中的每个对象调用析构函数,确保资源被正确释放。不会遗漏任何一个元素。 - 构造函数只在对象创建时调用一次,之后不能通过已存在的对象再次调用。它就像“出生证明”,每个对象只出生一次。
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
**析构函数无返回类型(**连 void都没有),不能返回值。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
1 |
|
this指针
this指针的实际作用
情况1:区分同名变量(最常用)
class Box {
private:
double length;
public:
void setLength(double length) {
this->length = length; // 左边的length是成员变量,右边的是参数
// 等价于:length = length; 会混淆!
}
};
情况2:返回对象本身(用于链式调用)
class Box {
public:
Box& setLength(double l) {
length = l;
return *this; // 返回当前对象本身
}
Box& setBreadth(double b) {
breadth = b;
return *this;
}
};
// 可以链式调用
Box box;
box.setLength(3.0).setBreadth(4.0); // 连续设置多个属性
情况3:在成员函数中明确表示当前对象
int compare(Box box) {
// 明确表示调用当前对象的Volume方法
return this->Volume() > box.Volume();
}
4. 你的代码执行过程分析
Box Box1(3.3, 1.2, 1.5); // 体积 = 3.3×1.2×1.5 = 5.94
Box Box2(8.5, 6.0, 2.0); // 体积 = 8.5×6.0×2.0 = 102.0
Box1.compare(Box2);
// 相当于:this->Volume() > box.Volume()
// 即:Box1.Volume() > Box2.Volume()
// 即:5.94 > 102.0 → 返回false(0)
所以输出结果是:”The volume of Box2 is greater than or equal to Box1”
5. 简单总结
场景
使用方式
目的
区分同名变量
this->成员变量
避免参数和成员变量名冲突
链式调用
return *this
支持连续调用多个方法
明确当前对象
this->方法()
代码更清晰易读
记住:this就是指向当前正在调用成员函数的那个对象的指针!
静态成员变量
静态成员变量可以有访问控制权限(public、private、protected)。如果静态成员被声明为private,则只能在类内部访问,不能直接从类外部访问;如果声明为public,才可以从外部访问。因此,访问控制权限是有意义的,它影响了静态成员的可见性和封装性。陈述说“均作为公有成员使用”是错误的,因为静态成员可以是私有的。
“内部静态类变量”可能指在函数内部定义的静态变量(static local variable),而“自动类变量”指自动变量(auto local variable,通常省略auto关键字)。
- 作用域:两者作用域相同,都局限于定义它们的函数内部。
- 生存期:自动变量的生存期是函数调用期间,函数结束即销毁;静态变量的生存期是整个程序运行期间,即使函数结束,变量仍存在。因此,作用域相同但生存期不同,陈述错误。
静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
静态成员函数与普通成员函数的区别:
- 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
- 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。
1 |
|
内联函数
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
引入内联函数的目的是为了解决程序中函数调用的效率问题,这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
- 1.在内联函数内不允许使用循环语句和开关语句;
- 2.内联函数的定义必须出现在内联函数第一次调用之前;
- 3.类结构中所在的类说明内部定义的函数是内联函数。
继承
一个派生类继承了所有的基类方法,但下列情况除外:
基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。
基类的友元函数。
继承类型
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,==基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。==
【你的就是我的,我的还是我的】除开私有
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
多继承
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
1 |
|
运算符重载
想象一下:
- 基本类型(int、float等)天生就会
+、-、*、/ - 但自定义的类(比如
Box盒子类)天生不会这些运算 - 运算符重载就是教会这些类如何使用运算符


比如:operator+、operator-、operator*
运算符重载很简单,教会类用运算符
函数名是 operator+对应符号
成员函数用this,非成员要两个参
返回新对象,运算自己定

&是取地址直接引用,效率更高,const保证了不被修改

例子:
1 | #include <iostream> |
输入输出流重载
输入输出流的引用
1 | // 错误的写法(不用引用) |
==最终这里给原来的对象返回的是它的引用==
链式调用
1 | // 普通函数调用 |
注意:
虽然运算符重载让自定义类型用起来像内置类型,但有几点不同:
不是所有运算符都能重载 可以重载:
+ - * / % = < > == != += -=等 不能重载:. :: ?: sizeof等
不能改变运算符的优先级
1 | a + b * c; // 永远是先乘后加 |
3.
不能改变运算符的操作数个数
1 | a + b; // 永远是两个操作数 |
4.
不能创建新运算符
1 | a ** b; // 错误!没有**运算符 |
内联函数
1 | // 判断字符是否为十六进制数字(0-9, A-F, a-f) |
==inline:告诉编译器把这个函数”内联”,简单理解就是”直接粘贴代码,不跳转调用”==
•
bool:函数返回布尔值(true/false)
•
char c:接收一个字符作为参数
•
逻辑是:字符是0-9 或者 字符是A-F 或者 字符是a-f
•
在ASCII码中,字符是可以直接比较大小的,因为每个字符都有对应的数字编码
实操
在类的声明中要用到的其他类,需要在这个类声明之前声明(至少是要声明一个简短的)
在类的声明中,构造函数不能忘,否则编译错误
然后输入这部分,要对应好哪个地方是哪个变量
同一个函数的函数声明,在所有地方都要一致,否则编译错误(如,不要漏掉const 和&这种)
1 |
|
成员函数可以直接调用类里面的变量,不用输入
调用的时候直接加个前缀它就知道你想用的是哪个类了,如c1.Point();【这后面的括号不能忘】
创建的对象必须要有名称【c1不能忘】
1 | Complex c1(r1, i1); |
输入输出的方向,cout<< cin>>
练习题语法
实验1,7-2
1 | /*要不要截断 |
int n;
while (cin >> n, n > 0) {
这是一个特别的while循环写法:
•
cin >> n:先读入一个整数n
•
==逗号,:表示执行多个表达式,但只以最后一个表达式作为循环条件==
•
实际效果:只要读取n成功且n>0,就继续循环
for (char c : s):
-
这是C++11引入的范围for循环
-
意思是:对于字符串s中的每一个字符c
-
相当于:
1 | for (int j = 0; j < s.size(); ++j) { |
cin.get();
•
cin:标准输入流对象
•
.:成员访问运算符
•
get():一个成员函数,用于读取一个字符
•
():函数调用运算符,没有参数表示读取一个字符
作用:
-
从输入缓冲区中读取一个字符(包括空格、制表符、换行符)
-
与>>运算符不同,>>会跳过空白字符
实验2,7-3