關鍵字庫函數
轉自:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/ej3mx1/
sizeof和strlen的區別
-
strlen
是頭文件<cstring> 中的函數,sizeof
是 C++ 中的運算符。 -
strlen
測量的是字符串的實際長度(其源代碼如下),以\0
結束。而sizeof
測量的是字符數組的分配大小。strlen
源代碼size_t strlen(const char *str) {size_t length = 0;while (*str++)++length;return length; }
舉例:
#include <iostream> #include <cstring>using namespace std;int main() {char arr[10] = "hello";cout << strlen(arr) << endl; // 5cout << sizeof(arr) << endl; // 10return 0; }
-
若字符數組
arr
作為函數的形參,sizeof(arr)
中arr
被當作字符指針來處理,strlen(arr)
中arr
依然是字符數組,從下述程序的運行結果中就可以看出。#include <iostream> #include <cstring>using namespace std;void size_of(char arr[]) {cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .cout << strlen(arr) << endl; }int main() {char arr[20] = "hello";size_of(arr); return 0; } /* 輸出結果: 8 5 */
-
strlen
本身是庫函數,因此在程序運行過程中,計算長度;而sizeof
在編譯時,計算長度; -
sizeof
的參數可以是類型,也可以是變量;strlen
的參數必須是char*
類型的變量。
lambda 表達式(匿名函數)的具體應用和使用場景
lambda
表達式的定義形式如下:
[capture list] (parameter list) -> reurn type {function body
}
其中:
- capture list:捕獲列表,指 lambda 表達式所在函數中定義的局部變量的列表,通常為空,但如果函數體中用到了
lambda
表達式所在函數的局部變量,必須捕獲該變量,即將此變量寫在捕獲列表中。捕獲方式分為:引用捕獲方式[&]
、值捕獲方式[=]
。 - return type、parameter list、function body:分別表示返回值類型、參數列表、函數體,和普通函數一樣。
舉例:
lambda
表達式常搭配排序算法使用。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;int main()
{vector<int> arr = {3, 4, 76, 12, 54, 90, 34};sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序for (auto a : arr){cout << a << " ";}return 0;
}
/*
運行結果:90 76 54 34 12 4 3
*/
explicit 的作用(如何避免編譯器進行隱式類型轉換)
作用:用來聲明類構造函數是顯示調用的,而非隱式調用,可以阻止調用構造函數時進行隱式轉換。只可用于修飾單參構造函數,因為無參構造函數和多參構造函數本身就是顯示調用的,再加上 explicit 關鍵字也沒有什么意義。
隱式轉換:
#include <iostream>
#include <cstring>
using namespace std;class A
{
public:int var;A(int tmp){var = tmp;}
};
int main()
{A ex = 10; // 發生了隱式轉換return 0;
}
上述代碼中,A ex = 10
; 在編譯時,進行了隱式轉換,將 10
轉換成 A
類型的對象,然后將該對象賦值給 ex
,等同于如下操作:
為了避免隱式轉換,可用 explicit
關鍵字進行聲明:
#include <iostream>
#include <cstring>
using namespace std;class A
{
public:int var;explicit A(int tmp){var = tmp;cout << var << endl;}
};
int main()
{A ex(100);A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requestedreturn 0;
}
static關鍵字
C和C++static的區別
-
在 C 語言中,使用
static
可以定義局部靜態變量、外部靜態變量、靜態函數 -
在 C++ 中,使用
static
可以定義局部靜態變量、外部靜態變量、靜態函數、靜態成員變量和靜態成員函數。因為 C++ 中有類的概念,靜態成員變量、靜態成員函數都是與類有關的概念。
static的作用
作用:
static
定義靜態變量,靜態函數。
- 保持變量內容持久:
static
作用于局部變量,改變了局部變量的生存周期,使得該變量存在于定義后直到程序運行結束的這段時間。
#include <iostream>
using namespace std;int fun(){static int var = 1; // var 只在第一次進入這個函數的時初始化var += 1;return var;
}int main()
{for(int i = 0; i < 10; ++i)cout << fun() << " "; // 2 3 4 5 6 7 8 9 10 11return 0;
}
-
隱藏:
static
作用于全局變量和函數,改變了全局變量和函數的作用域,使得全局變量和函數只能在定義它的文件中使用,在源文件中不具有全局可見性。(注:普通全局變量和函數具有全局可見性,即其他的源文件也可以使用。) -
static
作用于類的成員變量和類的成員函數,使得類變量或者類成員函數和類有關,也就是說可以不定義類的對象就可以通過類訪問這些靜態成員。注意:類的靜態成員函數中只能訪問靜態成員變量或者靜態成員函數,不能將靜態成員函數定義成虛函數。
#include<iostream>
using namespace std;class A
{
private:int var;static int s_var; // 靜態成員變量
public:void show(){cout << s_var++ << endl;}static void s_show(){cout << s_var << endl;// cout << var << endl; // error: invalid use of member 'A::a' in static member function. 靜態成員函數不能調用非靜態成員變量。無法使用 this.var// show(); // error: cannot call member function 'void A::show()' without object. 靜態成員函數不能調用非靜態成員函數。無法使用 this.show()}
};
int A::s_var = 1; // 靜態成員變量在類外進行初始化賦值,默認初始化為 0int main()
{ // cout << A::sa << endl; // error: 'int A::sa' is private within this contextA ex;ex.show();A::s_show();
}
static在類中使用的注意事項(定義、初始化和使用)
static 靜態成員變量:
-
靜態成員變量是在類內進行聲明,在類外進行定義和初始化,在類外進行定義和初始化的時候不要出現 static 關鍵字和private、public、protected 訪問規則。
-
靜態成員變量相當于類域中的全局變量,被類的所有對象所共享,包括派生類的對象。
-
靜態成員變量可以作為成員函數的參數,而普通成員變量不可以。
#include <iostream> using namespace std;class A { public:static int s_var;int var;void fun1(int i = s_var); // 正確,靜態成員變量可以作為成員函數的參數void fun2(int i = var); // error: invalid use of non-static data member 'A::var' }; int main() {return 0; }
-
靜態數據成員的類型可以是所屬類的類型,而普通數據成員的類型只能是該類類型的指針或引用。
#include <iostream> using namespace std;class A { public:static A s_var; // 正確,靜態數據成員A var; // error: field 'var' has incomplete type 'A'A *p; // 正確,指針A &var1; // 正確,引用 };int main() {return 0; }
static 靜態成員函數:
- 靜態成員函數不能調用非靜態成員變量或者非靜態成員函數,因為靜態成員函數沒有
this
指針。靜態成員函數做為類作用域的全局函數。 - 靜態成員函數不能聲明成虛函數
virtual
、const
函數和volatile
函數。
static 全局變量和普通全局變量的異同
相同點:
- 存儲方式:普通全局變量和 static 全局變量都是靜態存儲方式。
不同點:
- 作用域:普通全局變量的作用域是整個源程序,當一個源程序由多個源文件組成時,普通全局變量在各個源文件中都是有效的;靜態全局變量則限制了其作用域,即只在定義該變量的源文件內有效,在同一源程序的其它源文件中不能使用它。由于靜態全局變量的作用域限于一個源文件內,只能為該源文件內的函數公用,因此可以避免在其他源文件中引起錯誤。
- 初始化:靜態全局變量只初始化一次,防止在其他文件中使用。
const、define宏定義、typedef、inline
const作用及用法
作用:
const
修飾成員變量,定義成const
常量,相較于宏常量,可進行類型檢查,節省內存空間,提高了效率。const
修飾函數參數,使得傳遞過來的函數參數的值不能改變。const
修飾成員函數,使得成員函數不能修改任何類型的成員變量(mutable
修飾的變量除外),也不能調用非const
成員函數,因為非const
成員函數可能會修改成員變量。
在類中的用法:
const
成員變量:
const
成員變量只能在類內聲明、定義,在構造函數初始化列表中初始化。const
成員變量只在某個對象的生存周期內是常量,對于整個類而言卻是可變的,因為類可以創建多個對象,不同類的const
成員變量的值是不同的。因此不能在類的聲明中初始化const
成員變量,類的對象還沒有創建,編譯器不知道他的值。
const
成員函數:
-
不能修改成員變量的值,除非有 mutable 修飾;只能訪問成員變量。
-
不能調用非常量成員函數,以防修改成員變量的值。
#include <iostream>
using namespace std;class A
{
public:int var;A(int tmp) : var(tmp) {}void c_fun(int tmp) const // const 成員函數{var = tmp; // error: assignment of member 'A::var' in read-only object. 在 const 成員函數中,不能修改任何類成員變量。 fun(tmp); // error: passing 'const A' as 'this' argument discards qualifiers. const 成員函數不能調用非 const 成員函數,因為非 const 成員函數可能會修改成員變量。}void fun(int tmp){var = tmp;}
};int main()
{return 0;
}
define和const的區別
區別:
- 編譯階段:define 是在編譯預處理階段進行替換,const 是在編譯階段確定其值。
- 安全性:define 定義的宏常量沒有數據類型,只是進行簡單的替換,不會進行類型安全的檢查;const 定義的常量是有類型的,是要進行判斷的,可以避免一些低級的錯誤。
- 內存占用:define 定義的宏常量,在程序中使用多少次就會進行多少次替換,內存中有多個備份,占用的是代碼段的空間;const 定義的常量占用靜態存儲區的空間,程序運行過程中只有一份。
- 調試:define 定義的宏常量不能調試,因為在預編譯階段就已經進行替換了;const 定義的常量可以進行調試。
const
的優點:
- 有數據類型,在定義式可進行安全性檢查。
- 可調式。
- 占用較少的空間。
define和typedef的區別
-
原理:
#define
作為預處理指令,在編譯預處理時進行替換操作,不作正確性檢查,只有在編譯已被展開的源程序時才會發現可能的錯誤并報錯。typedef
是關鍵字,在編譯時處理,有類型檢查功能,用來給一個已經存在的類型一個別名,但不能在一個函數定義里面使用typedef
。 -
功能:
typedef
用來定義類型的別名,方便使用。#define
不僅可以為類型取別名,還可以定義常量、變量、編譯開關等。 -
作用域:
#define
沒有作用域的限制,只要是之前預定義過的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。 -
指針的操作:
typedef
和#define
在處理指針時不完全一樣
#include <iostream>
#define INTPTR1 int *
typedef int * INTPTR2;using namespace std;int main()
{INTPTR1 p1, p2; // p1: int *; p2: intINTPTR2 p3, p4; // p3: int *; p4: int *int var = 1;const INTPTR1 p5 = &var; // 相當于 const int * p5; 常量指針,即不可以通過 p5 去修改 p5 指向的內容,但是 p5 可以指向其他內容。const INTPTR2 p6 = &var; // 相當于 int * const p6; 指針常量,不可使 p6 再指向其他內容。return 0;
}
用宏實現比大小
#include <iostream>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;int main ()
{int var1 = 10, var2 = 100;cout << MAX(var1, var2) << endl;cout << MIN(var1, var2) << endl;return 0;
}
/*
程序運行結果:
100
10
*/
inline作用及使用方法
作用:
inline
是一個關鍵字,可以用于定義內聯函數。內聯函數,像普通函數一樣被調用,但是在調用時并不通過函數調用的機制而是直接在調用點處展開,這樣可以大大減少由函數調用帶來的開銷,從而提高程序的運行效率。
使用方法:
-
類內定義成員函數默認是內聯函數
在類內定義成員函數,可以不用在函數頭部加 inline 關鍵字,因為編譯器會自動將類內定義的函數(構造函數、析構函數、普通成員函數等)聲明為內聯函數,代碼如下:
#include <iostream> using namespace std;class A{ public:int var;A(int tmp){ var = tmp;} void fun(){ cout << var << endl;} };int main() { return 0; }
-
類外定義成員函數,若想定義為內聯函數,需用關鍵字聲明
當在類內聲明函數,在類外定義函數時,如果想將該函數定義為內聯函數,則可以在類內聲明時不加
inline
關鍵字,而在類外定義函數時加上inline
關鍵字。#include <iostream> using namespace std;class A{ public:int var;A(int tmp){ var = tmp;} void fun(); };inline void A::fun(){cout << var << endl; }int main() { return 0; }
另外,可以在聲明函數和定義函數的同時加上 inline;也可以只在函數聲明時加 inline,而定義函數時不加 inline。只要確保在調用該函數之前把 inline 的信息告知編譯器即可。
inline函數工作原理
- 內聯函數不是在調用時發生控制轉移關系,而是在編譯階段將函數體嵌入到每一個調用該函數的語句塊中,編譯器會將程序中出現內聯函數的調用表達式用內聯函數的函數體來替換。
- 普通函數是將程序執行轉移到被調用函數所存放的內存地址,當函數執行完后,返回到執行此函數前的地方。轉移操作需要保護現場,被調函數執行完后,再恢復現場,該過程需要較大的資源開銷。
宏定義(define)和內聯函數(inline)的區別
- 內聯函數是在編譯時展開,而宏在編譯預處理時展開;在編譯的時候,內聯函數直接被嵌入到目標代碼中去,而宏只是一個簡單的文本替換。
- 內聯函數是真正的函數,和普通函數調用的方法一樣,在調用點處直接展開,避免了函數的參數壓棧操作,減少了調用的開銷。而宏定義編寫較為復雜,常需要增加一些括號來避免歧義。
- 宏定義只進行文本替換,不會對參數的類型、語句能否正常編譯等進行檢查。而內聯函數是真正的函數,會對參數的類型、函數體內的語句編寫是否正確等進行檢查。
#include <iostream>#define MAX(a, b) ((a) > (b) ? (a) : (b))using namespace std;inline int fun_max(int a, int b)
{return a > b ? a : b;
}int main()
{int var = 1;cout << MAX(var, 5) << endl; cout << fun_max(var, 0) << endl; return 0;
}
/*
程序運行結果:
5
1*/
new/delete和malloc/free
new的作用
new
是 C++ 中的關鍵字,用來動態分配內存空間,實現方式如下:
int *p = new int[5];
new和malloc分別如何判斷是否申請到內存
malloc
:成功申請到內存,返回指向該內存的指針;分配失敗,返回 NULL 指針。new
:內存分配成功,返回該對象類型的指針;分配失敗,拋出 bac_alloc 異常。
delete 實現原理?delete 和 delete[] 的區別?
delete 的實現原理:
- 首先執行該對象所屬類的析構函數;
- 進而通過調用
operator delete
的標準庫函數來釋放所占的內存空間。
delete 和 delete [] 的區別:
delete
用來釋放單個對象所占的空間,只會調用一次析構函數;delete []
用來釋放數組空間,會對數組中的每個成員都調用一次析構函數。
new和malloc的區別
在使用的時候 new
、delete
搭配使用,malloc
、free
搭配使用。
malloc
、free
是庫函數,而new
、delete
是關鍵字。new
申請空間時,無需指定分配空間的大小,編譯器會根據類型自行計算;malloc
在申請空間時,需要確定所申請空間的大小。new
申請空間時,返回的類型是對象的指針類型,無需強制類型轉換,是類型安全的操作符;malloc
申請空間時,返回的是 void* 類型,需要進行強制類型的轉換,轉換為對象類型的指針。new
分配失敗時,會拋出bad_alloc
異常,malloc
分配失敗時返回空指針。- 對于自定義的類型,
new
首先調用operator new()
函數申請空間(底層通過malloc
實現),然后調用構造函數進行初始化,最后返回自定義類型的指針;delete
首先調用析構函數,然后調用operator delete()
釋放空間(底層通過free
實現)。malloc
、free
無法進行自定義類型的對象的構造和析構。 new
操作符從自由存儲區上為對象動態分配內存,而malloc
函數從堆上動態分配內存。(自由存儲區不等于堆)
(下表來自:C/C++——C++中new與malloc的10點區別)
特征 | new/delete | malloc/free |
---|---|---|
分配內存的位置 | 自由存儲區 | 堆 |
內存分配失敗返回值 | 完整類型指針 | void* |
內存分配失敗返回值 | 默認拋出異常 | 返回NULL |
分配內存的大小 | 由編譯器根據類型計算得出 | 必須顯式指定字節數 |
處理數組 | 有處理數組的new版本new[] | 需要用戶計算數組的大小后進行內存分配 |
已分配內存的擴充 | 無法直觀地處理 | 使用realloc簡單完成 |
是否相互調用 | 可以,看具體的operator new/delete實現 | 不可調用new |
分配內存時內存不足 | 客戶能夠指定處理函數或重新制定分配器 | 無法通過用戶代碼進行處理 |
函數重載 | 允許 | 不允許 |
構造函數與析構函數 | 調用 | 不調用 |
- malloc給你的就好像一塊原始的土地,你要種什么需要自己在土地上來播種
- 而new幫你劃好了田地的分塊(數組),幫你播了種(構造函數),還提供其他的設施給你使用:
malloc 的原理?malloc 的底層實現?
malloc 的原理:
- 當開辟的空間小于 128K 時,調用
brk()
函數,通過移動_enddata
來實現; - 當開辟空間大于 128K 時,調用
mmap()
函數,通過在虛擬地址空間中開辟一塊內存空間來實現。
malloc 的底層實現:
brk()
函數實現原理:向高地址的方向移動指向數據段的高地址的指針_enddata
。mmap
內存映射原理:- 進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域;
- 調用內核空間的系統調用函數
mmap()
,實現文件物理地址和進程虛擬地址的一一映射關系; - 進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝。
class、struct、union
C 和 C++ struct 的區別?
- 在 C 語言中 struct 是用戶自定義數據類型;在 C++ 中 struct 是抽象數據類型,支持成員函數的定義。
- C 語言中 struct 沒有訪問權限的設置,是一些變量的集合體,不能定義成員函數;C++ 中 struct 可以和類一樣,有訪問權限,并可以定義成員函數。
- C 語言中 struct 定義的自定義數據類型,在定義該類型的變量時,需要加上 struct 關鍵字,例如:struct A var;,定義 A 類型的變量;而 C++ 中,不用加該關鍵字,例如:A var;
為什么有了 class 還保留 struct?
C++ 是在 C 語言的基礎上發展起來的,為了與 C 語言兼容,C++ 中保留了 struct
。
struct 和 union 的區別
說明:union 是聯合體,struct 是結構體。
區別:
- 聯合體和結構體都是由若干個數據類型不同的數據成員組成。使用時,聯合體只有一個有效的成員;而結構體所有的成員都有效。
- 對聯合體的不同成員賦值,將會對覆蓋其他成員的值,而對于結構體的對不同成員賦值時,相互不影響。
- 聯合體的大小為其內部所有變量的最大值,按照最大類型的倍數進行分配大小;結構體分配內存的大小遵循內存對齊原則。
#include <iostream>
using namespace std;typedef union
{char c[10];char cc1; // char 1 字節,按該類型的倍數分配大小
} u11;typedef union
{char c[10];int i; // int 4 字節,按該類型的倍數分配大小
} u22;typedef union
{char c[10];double d; // double 8 字節,按該類型的倍數分配大小
} u33;typedef struct s1
{char c; // 1 字節double d; // 1(char)+ 7(內存對齊)+ 8(double)= 16 字節
} s11;typedef struct s2
{char c; // 1 字節char cc; // 1(char)+ 1(char)= 2 字節double d; // 2 + 6(內存對齊)+ 8(double)= 16 字節
} s22;typedef struct s3
{char c; // 1 字節double d; // 1(char)+ 7(內存對齊)+ 8(double)= 16 字節char cc; // 16 + 1(char)+ 7(內存對齊)= 24 字節
} s33;int main()
{cout << sizeof(u11) << endl; // 10cout << sizeof(u22) << endl; // 12cout << sizeof(u33) << endl; // 16cout << sizeof(s11) << endl; // 16cout << sizeof(s22) << endl; // 16cout << sizeof(s33) << endl; // 24cout << sizeof(int) << endl; // 4cout << sizeof(double) << endl; // 8return 0;
}
class 和 struct 的異同
- struct 和 class 都可以自定義數據類型,也支持繼承操作。
- struct 中默認的訪問級別是 public,默認的繼承級別也是 public;class 中默認的訪問級別是 private,默認的繼承級別也是 private。
- 當 class 繼承 struct 或者 struct 繼承 class 時,默認的繼承級別取決于 class 或 struct 本身, class(private 繼承),struct(public 繼承),即取決于派生類的默認繼承級別。
class
可以用于定義模板參數,struct
不能用于定義模板參數。
struct A{};
class B : A{}; // private 繼承
struct C : B{}; // public 繼承
volatile
valatile的作用?是否具有原子性?
-
volatile
的作用:當對象的值可能在程序的控制或檢測之外被改變時,應該將該對象聲明為volatile
,告知編譯器不應對這樣的對象進行優化。 -
volatile
不具有原子性。 -
volatile
對編譯器的影響:使用該關鍵字后,編譯器不會對相應的對象進行優化,即不會將變量從內存緩存到寄存器中,防止多個線程有可能使用內存中的變量,有可能使用寄存器中的變量,從而導致程序錯誤。
什么情況下一定要用 volatile, 能否和 const 一起使用?
使用 volatile
關鍵字的場景:
- 當多個線程都會用到某一變量,并且該變量的值有可能發生改變時,需要用
volatile
關鍵字對該變量進行修飾; - 中斷服務程序中訪問的變量或并行設備的硬件寄存器的變量,最好用
volatile
關鍵字修飾。
volatile
關鍵字和 const
關鍵字可以同時使用,某種類型可以既是 volatile
又是 const
,同時具有二者的屬性。
extern、sizeof、memmove、strcpy、auto雜問
extern C 的作用?
當 C++ 程序 需要調用 C 語言編寫的函數,C++ 使用鏈接指示,即 extern "C"
指出任意非 C++ 函數所用的語言。
舉例:
// 可能出現在 C++ 頭文件<cstring>中的鏈接指示
extern "C"{int strcmp(const char*, const char*);
}
sizeof(1==1) 在 C 和 C++ 中分別是什么結果?
C 語言代碼:
#include<stdio.h>void main(){printf("%d\n", sizeof(1==1));
}/*
運行結果:
4
*/
C++ 代碼:
#include <iostream>
using namespace std;int main() {cout << sizeof(1==1) << endl;return 0;
}/*
1
*/
C語言:
sizeof(1 == 1) === sizeof(1)按照整數處理,所以是4字節,這里也有可能是8字節(看操作系統)
C++:
因為有bool 類型,sizeof(1 == 1) == sizeof(true) 按照bool類型處理,所以是1個字節
memmove與memcpy
詳見:memmove 和 memcpy的區別以及處理內存重疊問題
strcpy 函數有什么缺陷?
strcpy
函數的缺陷:strcpy
函數不檢查目的緩沖區的大小邊界,而是將源字符串逐一的全部賦值給目的字符串地址起始的一塊連續的內存空間,同時加上字符串終止符,會導致其他變量被覆蓋。
#include <iostream>
#include <cstring>
using namespace std;int main()
{int var = 0x11112222;char arr[10];cout << "Address : var " << &var << endl;cout << "Address : arr " << &arr << endl;strcpy(arr, "hello world!");cout << "var:" << hex << var << endl; // 將變量 var 以 16 進制輸出cout << "arr:" << arr << endl;return 0;
}/*
Address : var 0x23fe4c
Address : arr 0x23fe42
var:11002164
arr:hello world!
*/
說明:從上述代碼中可以看出,變量 var
的后六位被字符串 "hello world!"
的 "d!\0"
這三個字符改變,這三個字符對應的 ascii 碼的十六進制為:\0
(0x00),!
(0x21),d
(0x64)。
原因:變量 arr
只分配的 10 個內存空間,通過上述程序中的地址可以看出 arr
和 var
在內存中是連續存放的,但是在調用 strcpy
函數進行拷貝時,源字符串 "hello world!"
所占的內存空間為 13,因此在拷貝的過程中會占用 var
的內存空間,導致 var
的后六位被覆蓋。
auto 類型推導的原理
auto
類型推導的原理:
編譯器根據初始值來推算變量的類型,要求用 auto
定義變量時必須有初始值。編譯器推斷出來的 auto
類型有時和初始值類型并不完全一樣,編譯器會適當改變結果類型使其更符合初始化規則。