0.1. Những “thông tin” sử dụng trong ngôn ngữ C

Estimated reading: 41 minutes 364 views
				
					sizeof(Kiểu dữ liệu) 
sizeof Tên biến số				
			

Chương trình được sinh ra để xử lý một thông tin nào đó, thông tin đó trong C có thể là biến số (variable) hoặc hằng số (constant). Đoạn chương trình xử lý thông tin đó là hàm số (function).

Kiểu dữ liệu trong C

Dù là biến số hay hằng số đều chiếm một vùng không gian bộ nhớ (gọi tắt là vùng nhớ). Độ lớn và đặc trưng của từng vùng nhớ được biểu thị bằng “kiểu dữ liệu”. Hình bên dưới là các kiểu dữ liệu sử dụng trong C, trong đó “Kiểu cơ bản” bao gồm số thực và số nguyên là các kiểu dữ liệu đầu tiên cần ghi nhớ. Số nguyên có kiểu char, shortint và long; trong đó kiểu char còn được dùng để biểu thị ký tự.

Độ lớn của số nguyên phụ thuộc vào kích cỡ (size) của vùng nhớ kiểu dữ liệu đó. Ví dụ dải số của một số tự nhiên (không dấu) 1 byte (8 bit) là 0~255; trong khi 2 byte (16 bit) là 0~65.535. Trong ANSI C, kích cỡ của các loại kiểu số nguyên không được qui định cụ thể nhưng phải thoả mãn các yêu cầu sau đây:

  • Dải dữ liệu tối thiểu:
    • signed char: -127~127
    • unsigned char: 0~255
    • short: -32.767~32.767
    • unsigned short: 0~65.535
    • long: -2.147.483.647~2.147.483.647
    • unsigned long: 0~4.294.967.295
  • Kích cỡ của shortintlong
    • Kích cỡ short ≦ Kích cỡ int ≦ Kích cỡ long
    • Kích cỡ short ≧ 2 byte (Trường hợp 1 byte = 8 bit)
    • Kích cỡ int ≧ 2 byte (Trường hợp 1 byte = 8 bit)
    • Kích cỡ long ≧ 4 byte (Trường hợp 1 byte = 8 bit)

Với kiểu số thực thì phạm vi số thực của long double chứa được phạm vi của doubledouble chứa được phạm vi của floatNgoài ra dùng toán tử sizeof chúng ta có thể lấy ra được kích cỡ (tính theo byte) của kiểu dữ liệu hoặc biến số.

				
					sizeof (Tên Kiểu Dữ Liệu);
sizeof Biến;				
			

Khai báo biến, toán tử và công thức

Khai báo biến

Dể sử dụng dữ liệu của một kiểu dữ liệu nào đó, chúng ta phải khai báo biến của kiểu đó. Tên biến bao gồm chữ cái Alphabet, chữ số và dấu “_”, tuy nhiên chữ số không được xuất hiện ở đầu tên biến. Một tên do Dev định nghĩa gọi là định danh (identifier), trong khi những tên đã được định nghĩa sẵn trong C như int được gọi là từ khoá (keyword). Chúng ta không thể định nghĩa định danh trùng với từ khoá. Cách khai báo biến giống như ô bên dưới.

				
					(1) Khai báo biến không khởi tạo giá trị biến.
KiểuDữLiệu TênBiếnSố;

(2) Khai báo biến khởi tạo giá trị biến.
KiểuDữLiệu TênBiếnSố;
Ví dụ:
int i; 
char c1;
int j = 0;
char c2 = 'A';
char c3 = 1;
double d = 0.1;				
			

Công thức

Ta có thể khai báo biến mà không có giá trị khởi tạo theo phương pháp (1) hoặc có giá trị khởi tạo như (2). Không khởi tạo biến số sẽ có giá trị bất định, vì vậy khởi tạo biến cũng là một thói quen tốt nên làm.

Để khởi tạo biến hoặc gán giá trị cho một biến số chúng ta dùng toán tử gán “=”. Ví dụ muốn gán giá trị “1” cho biến “i” ta dùng công thức (expression) sau:

i = 1;

Công thức trên sẽ gán giá trị của bên phải dấu “=” (cụ thể là “1”) vào biến số bên trái dấu “=’ (cụ thể là “i”), sau khi thực hiện công thức, biến số i sẽ có có giá trị là 1.

Các loại Toán tử

Các toán tử dùng để xử lý số (numeric operators) được liệt kê như trong bảng bên dưới. Bốn phép toán +, -, *, / là các toán tử thực hiện phép cộng, trừ, nhân và chia. Toán tử % là toán tử dùng để lấy số dư (remainder operator), và các toán hạng (operand) của toán tử này phải là số nguyên.

Bằng cách viết:

sốNguyên1 % sốNguyên2

ta sẽ tính được phần dư khi sốNguyên1 được chia cho sốNguyên2.

Loại toán tửNội dung
Cơ bản+, -, *, /
Phép chia dư% (toán hạng là số nguyên)
Bit&, |, ^, ~ (toán hạng phải là số nguyên)
Dịch Bit>>, << (toán hạng phải là số nguyên)
Tăng++
Giảm−−
Gán phức hợp+=, -=, *=, /=, <<=, >>=, &=, ^=, I=

Toán tử bit và dịch bit

Toán tử bit là những toán tử thao tác trên từng bit của toán hạng như trong bảng bên dưới, chú ý rằng toán hạng là số nguyên với nhiều bit.
ABAND (A&B)OR (A|B)XOR (A^B)NOT (~A)
111100
100110
010111
000001

Toán tử dịch bit có 2 loại và cách dùng như sau:

ToánTử >> SốBitCầnDịch;
ToánTử << SốBitCầnDịch;
Ví dụ biến số a và b có 8 bit là: 00011010
a >> 1; // Kết quả là 00001101
b << 1; // Kết quả là 00110100

Toán tử tăng (++) và toán tử giảm (–)

Toán tử tăng ++ và toán tử giảm -- lần lượt làm tăng hoặc giảm giá trị được lưu trong biến kiểu vô hướng (scalar type) thêm 1 đơn vị. Ví dụ, khi muốn tăng giá trị của biến kiểu int lên 1, thay vì viết:

i = i + 1

ta có thể viết:

++i

Điều cần lưu ý là toán tử ++-- có thể được đặt trước toán hạng (dạng tiền tố — prefix) hoặc đặt sau toán hạng (dạng hậu tố — postfix), và giá trị của biểu thức sẽ khác nhau giữa hai dạng.

Trong dạng tiền tố, “giá trị sau khi đã tăng (hoặc giảm) 1” sẽ trở thành giá trị của biểu thức.
Trong dạng hậu tố, “giá trị trước khi tăng (hoặc giảm) 1” sẽ trở thành giá trị của biểu thức.

Nói cách khác, khi giá trị của i là 3:

(++i) * thì giá trị biểu thức là (4 × 5) = 20.

Nhưng:

(i++) * thì giá trị biểu thức là (3 × 5) = 15.

Việc tăng giá trị bằng ++ được gọi là “increment (インクリメント)”, việc giảm giá trị bằng -- được gọi là “decrement (デクリメント)”.

Toán tử gán kết hợp (複合代入演算子)

Toán tử gán kết hợp có dạng @=, trong đó @ là một toán tử.

Nó được dùng như:

a @= b

và đây là dạng viết rút gọn của:

a = a @ b

Câu lệnh (文)

Để phân tách các biểu thức, ta đặt dấu chấm phẩy (;) ở cuối mỗi biểu thức và viết:

biểu thức;

và điều này trở thành một đơn vị gọi là “statement (câu lệnh)”. Việc xử lý trong ngôn ngữ C được mô tả bằng cách viết các câu lệnh nối tiếp nhau.

Hằng số và Chú thích

Hằng số (constant) và enum

Thông tin mà không thể thay đổi giá trị được gọi là hằng số. Việc viết trực tiếp một giá trị số như:

12345

là một ví dụ về hằng số.
Hằng số ký tự được viết bằng cách đặt một ký tự duy nhất trong dấu nháy đơn ('):

'a'

Nếu khai báo biến với từ khóa dự trữ const, bạn có thể khai báo biến đó như một hằng số có độ “cứng” cao, tức là không thể thay đổi giá trị. Hằng số được khai báo bằng const được gọi là “hằng số const (const定数)”.

const Kiểu_Dữ_Liệu Tên_Hằng_Số = Giá_Trị;

				
					const int i = 0;
const char c1 = 'a';
const double d = 0.1;				
			

Ngoài ra, còn có một kiểu đặc biệt để biểu diễn hằng số nguyên (integer constants), đó là kiểu liệt kê (enumeration type). Kiểu liệt kê của C, về mặt nội bộ, được biểu diễn như một số nguyên kiểu int. Cách định nghĩa (定義) kiểu liệt kê như sau:

enum Tên_Liệt_Kê {

Hằng_Số_0 = Giá_Trị_0,

Hằng_Số_1 = Giá_Trị_1,

...

Hằng_Số_n = Giá_Trị_n

};

 Ví dụ: Mã quốc gia

				
					enum Country {
    America, /* Giá trị hằng sẽ là 0 */
    Japan,   /* Giá trị hằng sẽ là 1 */
    UK,      /* Giá trị hằng sẽ là 2 */
    USA = 0  /* Giá trị hằng sẽ là 0 */
};				
			

Ghi chú:

  • Phần = Giá_Trị có thể được lược bỏ. Trong trường hợp đó, giá trị của hằng được tính bằng giá trị của hằng ngay trước đó cộng thêm 1.

  • Nếu hằng đầu tiên lược bỏ = Giá_Trị, thì giá trị mặc định được xem là 0.

  • Các hằng được định nghĩa trong enum phải có ít nhất 1 phần tử.

  • Phần enum Tên_Liệt_Kê chính là tên kiểu.

Cách khai báo biến kiểu liệt kê (宣言) như sau:

enum Tên_Liệt_Kê Tên_Biến;
enum Tên_Liệt_Kê Tên_Biến = Giá_Trị;

Chú ý rằng các hằng và biến của kiểu liệt kê được biểu diễn nội bộ dưới dạng kiểu int. Ví dụ: Sử dụng kiểu enum Country trong ví dụ trước:

				
					enum Country myCountry = Japan;
myCountry = -1;
/* Cần chú ý rằng vẫn có thể gán giá trị nguyên ngoài phạm vi của enum */				
			

Bạn cũng có thể định nghĩa kiểu (type definition)khai báo biến (variable declaration, bao gồm cả khởi tạo – initialization) cùng lúc, như ví dụ sau:

				
					enum Country {
    America, Japan, UK, USA = 0
} myCountry;				
			

Bên cạnh đó, tuy không liên quan trực tiếp đến hằng số (constant / 定数), nhưng tương tự như const, còn có từ khóa dùng để bổ nghĩa (qualify / 修飾) cho biến là volatile.

volatile là một chỉ thị dùng để ngăn không cho trình biên dịch (compiler / コンパイラ) loại bỏ biến đó trong quá trình tối ưu hóa (optimization).
Khi được sử dụng kết hợp với const, cách chỉ định này được gọi là chỉ định const/volatile (const/volatile qualifier, cv指定).

Chú thích

Nhân tiện, trong ví dụ trên, phần mô tả được đặt giữa "/*""*/".
Phần nội dung nằm trong khoảng từ "/*" đến "*/" được xử lý như chú thích (comment) và không ảnh hưởng đến chương trình.

Hãy viết các nội dung dễ hiểu, rõ ràng dưới dạng chú thích để giúp việc đọc và bảo trì mã nguồn thuận tiện hơn. Cần lưu ý rằng không thể viết chú thích lồng nhau (nested comment/ 入れ子).
Nói cách khác, không được phép đặt chú thích bên trong chú thích. Ví dụ sau là không hợp lệ:

/* /* */ */
Vì vậy, hãy cẩn thận để không viết chú thích theo cách như trên.

Kiểu tập hợp (cấu trúc và union)

Kiểu cấu trúc (struct)

Có những trường hợp ta muốn tập hợp nhiều thông tin lại thành một kiểu duy nhất. Ví dụ, để biểu diễn một “cá nhân”, sẽ rất tiện lợi nếu có thể gom các thông tin như tên, tuổi, giới tính, địa chỉ, số điện thoại, v.v. lại thành một kiểu gọi là kiểu Person.

Trong ngôn ngữ C, kiểu dữ liệu mới được tạo ra bằng cách gom nhiều biến hoặc hằng số lại với nhau được gọi là kiểu cấu trúc (構造体型 / structure type).
Cách định nghĩa kiểu cấu trúc và cách khai báo biến của nó được trình bày trong hình bên dưới.

				
					// (1) Cách định nghĩa biến kiểu cấu trúc
struct Tên_Cấu_Trúc {
    Kiểu_Dữ_Liệu  Tên_biến_thành_viên0;
    Kiểu_Dữ_Liệu  Tên_biến_thành_viên1;
    ...
    Kiểu_Dữ_Liệu  Tên_biến_thành_viênn;
};
// Các thành viên (member) phải có ít nhất 1 phần tử
// Phần "struct Tên_Cấu_Trúc" trở thành tên kiểu dữ liệu (type name)
// Ví dụ: Kiểu số phức có phần thực và phần ảo
struct ComplexNumber {
    double real;   /* phần thực (real part / 実数部) */
    double image;  /* phần ảo (imaginary part / 虚数部) */
};

// (2) Cách khai báo biến kiểu cấu trúc
struct Tên_Cấu_Trúc Tên_biến_cấu_trúc;
struct Tên_Cấu_Trúc Tên_biến_cấu_trúc = {
    Giá_trị_khởi_tạo_0,
    Giá_trị_khởi_tạo_1,
    ...
    Giá_trị_khởi_tạo_n
};
struct ComplexNumber i = { 0.0, 1.0 };
struct Tên_Cấu_Trúc Tên_biến_cấu_trúc
    = Biến_cấu_trúc_hoặc_hằng_số_cùng_kiểu;

// (3) Tham chiếu đến thành viên của cấu trúc
// Tên_biến_cấu_trúc.Tên_biến_thành_viên;
// Ví dụ: Nhân độ lớn của số phức c lên k lần
c.real  = c.real  * k;
c.image = c.image * k;

				
			

Mỗi biến được chứa trong cấu trúc được gọi là thành viên (member / メンバ). Ta có thể thao tác với các thành viên bằng toán tử ., theo cú pháp:

Tên_biến_cấu_trúc.Tên_thành_viên

Ta cũng có thể thực hiện đồng thời việc định nghĩa kiểu và khai báo biến (bao gồm cả khởi tạo) như sau:

				
					struct ComplexNumber {
    double real;
    double image;
} i = { 0.0, 1.0 };				
			

Cần lưu ý rằng kích thước của một cấu trúc đôi khi có thể lớn hơn tổng kích thước của các thành viên. Nguyên nhân là vì, để CPU truy cập hiệu quả hơn, trình biên dịch (compiler / コンパイラ) có thể chèn các vùng bộ nhớ không sử dụng (padding / パディング) giữa các thành viên, nhằm sắp xếp mỗi thành viên tại vị trí bộ nhớ tối ưu.

Kiểu union (cộng dụng)

Đôi khi, có trường hợp ta muốn sử dụng linh hoạt cùng một vùng bộ nhớ (memory region / メモリ領域) đã được cấp phát cho một biến, cho nhiều mục đích khác nhau. Ví dụ, có lúc muốn dùng một biến để lưu trữ dữ liệu ký tự (character data / 文字データ), nhưng lúc khác lại muốn dùng chính biến đó để lưu trữ dữ liệu số thực (floating-point data / 実数データ). Kiểu dữ liệu có thể dùng cho mục đích như vậy được gọi là kiểu cộng dụng (union type / 共用体型). Cách định nghĩa kiểu cộng dụng và khai báo biến kiểu cộng dụng được trình bày bên dưới.

				
					// (1) Cách định nghĩa kiểu union
union Tên_union {
    Kiểu_dữ_liệu_0 Tên_biến_thành_viên_0;
    Kiểu_dữ_liệu_1 Tên_biến_thành_viên_1;
    ...
    Kiểu_dữ_liệu_n Tên_biến_thành_viên_n;
};
// Xử lý cùng một vùng bộ nhớ (memory region / メモリ領域) như ký tự (char / 文字) hoặc số thực (double / 実数).
union CharOrDouble {
    char c;      /* dùng như dữ liệu ký tự */
    double d;  /* dùng như dữ liệu số thực */
};

// (2) Cách khai báo biến kiểu union
union Tên_union Tên_biến_union;
union Tên_union Tên_biến_union ={
    Giá_trị_khởi_tạo_0;
};
union Tên_union Tên_biến_union = Biến_union_hoặc_hằng_số_cùng_kiểu;
// Việc khởi tạo chỉ có thể thực hiện cho thành viên đầu tiên của union. Ví dụ:
union CharOrDouble cod = { 'a' };

// (3) Tham chiếu đến thành viên của cộng dụng
Tên_biến_union.Tên_thành_viên;
// Ví dụ: Sử dụng biến cod kiểu CharOrDouble.
cod.c = 'b';   /* xử lý cod như biến kiểu char */
cod.d = 2.0; /* xử lý cod như biến kiểu double */
				
			

Cũng giống như kiểu liệt kê (enumeration type / 列挙型) và kiểu cấu trúc (structure type / 構造体型), với kiểu cộng dụng (union / 共用体), ta có thể thực hiện đồng thời việc định nghĩa kiểu và khai báo biến (bao gồm cả khởi tạo / initialization) như sau:

				
					union CharOrDouble {
    char c;
    double d;
} cod = { 'a' };				
			

Kiểu mảng(配列型 / array type)

Kiểu cấu trúc(構造体型 / structure type)chủ yếu là tập hợp các biến có kiểu khác nhau, nhưng trong nhiều trường hợp, ta muốn gom nhiều biến có cùng một kiểu để xử lý cùng lúc. Ví dụ, một danh sách hội viên có thể được biểu diễn như một tập hợp gồm nhiều dữ liệu kiểu Person, mỗi dữ liệu tương ứng với một cá nhân. 

Tập hợp các biến cùng kiểu được gọi là kiểu mảng(配列型 / array type). 

Các biến chứa trong mảng được gọi là phần tử(要素 / element), và có thể được chỉ định bằng chỉ số(添え字 / index), chẳng hạn như “phần tử thứ 3”, “phần tử thứ 10”, v.v. 

Ví dụ, nếu tên mảng là a, thì phần tử thứ i có thể được tham chiếu bằng toán tử [] như sau:

a[i]

Cần lưu ý rằng chỉ số của mảng luôn bắt đầu từ 0.

Tổng quan về kiểu mảng được minh họa bên dưới.

				
					// (1) Cách định nghĩa biến kiểu mảng
Kiểu_dữ_liệu Tên_biến_mảng[ Số_lượng_phần_tử ];

Kiểu_dữ_liệu Tên_biến_mảng[ Số_lượng_phần_tử ] =
{
    Giá_trị_khởi_tạo_0,
    ...,
    Giá_trị_khởi_tạo_n-1
};
// Số lượng phần tử của mảng phải là hằng số nguyên(integer constant)và ≧ 1.

// (2) Tham chiếu phần tử thứ j của mảng
Tên_biến_mảng[j];
// j được gọi là chỉ số(添字 / index)
// Đối với mảng có n phần tử, các chỉ số hợp lệ nằm trong khoảng 0 đến (n−1).

// (3) Ví dụ: Mảng a gồm 5 phần tử kiểu int
int a[5] = {10, 5, 6, 9, 1};				
			
10 5 6 9 1
a[0] a[1] a[2] a[3] a[4]

Do đó, đối với mảng a có n phần tử, phần tử đầu tiên là phần tử thứ 0, nên được biểu diễn là a[0], phần tử cuối cùng là phần tử thứ (n−1), nên được biểu diễn là a[n-1]. Nói cách khác, phạm vi chỉ số hợp lệ(有効な添え字 / valid index range)đối với mảng có n phần tử là 0 ~ n−1.

Điều cần phải chú ý là, ngay cả khi ghi một giá trị nguyên không hợp lệ vào bên trong toán tử [], thì về mặt cú pháp(文法 / syntax) cũng không bị coi là sai. Ví dụ, đối với mảng được khai báo như sau:

int a[10];

thì việc ghi:

a[13] = 1;

về mặt cú pháp cũng không phải là lỗi. Tuy nhiên, nếu thực hiện thao tác như vậy, bạn sẽ ghi giá trị ra ngoài vùng bộ nhớ(memory region) mà mảng chiếm giữ, và điều này sẽ dẫn đến hành vi bất thường của chương trình(異常動作 / undefined behavior). Vì vậy, khi sử dụng chỉ số mảng(添え字 / index), luôn luôn phải cẩn thận chỉ định giá trị thích hợp.

Ngoài ra, mảng có các phần tử kiểu T thường được gọi là “mảng kiểu T”(T型配列 / array of type T). Cũng như đã đề cập trước đó, giữa các thành viên của cấu trúc(structure members)có thể tồn tại khoảng trống(padding), nhưng các phần tử của mảng thì được đảm bảo xếp liên tiếp, không có khe hở. Do đó, kích thước của mảng luôn luôn bằng:

sizeof(kiểu_phần_tử) × số_lượng_phần_tử

Kiểu chuỗi ký tự (文字列 / string)

Chuỗi ký tự, tức chuỗi (string), được biểu diễn bằng mảng (array) kiểu char. Trong ngôn ngữ C, hằng chuỗi (string literal) được viết bằng cách đặt một dãy ký tự trong dấu ngoặc kép (""), ví dụ:

"abcd"

Dữ liệu này được biểu diễn dưới dạng một mảng char trong đó một giá trị 0 được thêm vào cuối chuỗi để chỉ ra vị trí kết thúc chuỗi (giá trị này trong cách biểu diễn hệ thập lục phân của C là 0x00).

Cần lưu ý rằng kích thước của mảng sẽ lớn hơn số ký tự của chuỗi đúng 1 đơn vị.

Mảng kiểu char có thể được khởi tạo bằng hằng chuỗi. Trong trường hợp này, có thể lược bỏ khai báo kích thước mảng. Khi đó, mảng sẽ có (số ký tự của hằng chuỗi + 1) phần tử.

Giải thích chi tiết như sau:

(1) Hằng chuỗi (string literal / 文字列リテラル)

"nội_dung_chuỗi"

(2) Chuỗi được biểu diễn dưới dạng mảng có phần tử kiểu char
※ Ở cuối chuỗi luôn luôn có một giá trị số 0 (null terminator).
Vì vậy, kích thước của chuỗi = (số ký tự + 1).

Ví dụ:

"abcd"  sẽ là

a’ b’ c’ d’ 0x00

(3) Cách khai báo biến chuỗi kèm khởi tạo (initialization)

char Ten_chuoi[ kich_thuoc ] = “hang_chuoi”;

char Ten_chuoi[] = “hang_chuoi”;

※ Khi chỉ định kích thước mảng, phải chỉ định giá trị lớn hơn số ký tự của chuỗi.
※ Khi bỏ qua kích thước mảng, thì được hiểu là đã khai báo một mảng char có (số ký tự + 1) phần tử.

Ví dụ:

char name[5] = “Vaix”;

char name[] = “VaixVietnam”;

				
					// (1) Hằng chuỗi (string literal / 文字列リテラル)
"nội_dung_chuỗi"
				
			

Kiểu con trỏ (pointer type)

Không gian bộ nhớ và biến kiểu con trỏ

Trong không gian bộ nhớ (memory space), mỗi đơn vị nhỏ nhất (thông thường là 1 byte) đều được gán một số thứ tự liên tiếp bắt đầu từ 0. Số thứ tự này được gọi là địa chỉ bộ nhớ (memory address / メモリアドレス).

Hình dưới minh họa một ví dụ về không gian bộ nhớ. Trong ví dụ này:

  • Biến kiểu charc được cấp phát 1 byte tại địa chỉ thứ x

  • Biến kiểu shorts được cấp phát 2 byte liên tiếp, bắt đầu từ địa chỉ thứ y

Ngôn ngữ C được thiết kế để có thể mô tả cả những chương trình liên quan chặt chẽ đến phần cứng (hardware). Trong các chương trình như vậy, cần có phương tiện cho phép xử lý trực tiếp địa chỉ bộ nhớ, từ đó truy cập tự do đến các biến nằm trong bộ nhớ.

Phương tiện mà ngôn ngữ C cung cấp cho mục đích này chính là biến kiểu con trỏ (pointer variable / ポインタ型変数), dùng để biểu diễn địa chỉ bộ nhớ.

(1) Sơ đồ Khái niệm về Không gian Bộ nhớ

  • Địa chỉ bộ nhớ được gán cho từng byte.

  • Biến kiểu char và biến kiểu short có kích thước lần lượt là 1 byte và 2 byte (Đây là ví dụ về một hệ thống như vậy).

  • Không gian Bộ nhớ:

    • 0xAA tại địa chỉ x (Biến char c)

    • 0x11 tại địa chỉ y (Phần cao của Biến short s)

    • 0x11 tại địa chỉ y+1 (Phần thấp của Biến short s)

(2) Khai báo Biến Con trỏ Cú pháp: 

Kiểu_dữ_liệu_được_trỏ * Tên_biến_pointer;

Kiểu_dữ_liệu_được_trỏ * Tên_biến_pointer = pointer_cùng_kiểu;

Kiểu_dữ_liệu_được_trỏ * Tên_biến_pointer = pointer_kiểu_void*;

  • Lưu ý: Phần “Kiểu_dữ_liệu_được_trỏ *” là kiểu dữ liệu.

(3) Lấy Địa chỉ Bộ nhớ của Biến
Cú pháp: &Tên_biến
Ví dụ cho biến con trỏ cpsp trong sơ đồ trên:

				
					char c = 0xAA;
short s = 0x1111;
char *cp = &c;
short *sp = &s;				
			

(4) Truy cập vùng nhớ mà con trỏ trỏ tới dưới dạng biến (Giải tham chiếu gián tiếp) Cú pháp: *Tên_biến_pointer  Ví dụ: Ghi giá trị 0x01 vào biến c trong sơ đồ trên:

				
					char c = 0xAA;
char *cp = &c;
*cp = 0x01;				
			

Sử dụng biến kiểu con trỏ

Biến kiểu con trỏ (pointer type / ポインタ型変数) được khai báo như sau:
Ví dụ:

char* cp; /* kiểu của biến con trỏ cp là char* */
int* ip; /* kiểu của biến con trỏ ip là int* */

Để lấy địa chỉ bộ nhớ (memory address / メモリアドレス) của một biến cụ thể làm giá trị con trỏ (pointer value), ta sử dụng toán tử lấy địa chỉ (address-of operator / アドレス演算子) và viết:

&ten_bien

Ngoài ra, để tham chiếu đến biến mà biến con trỏ đang trỏ tới, ta sử dụng toán tử gián tiếp * (indirection operator / 間接参照演算子) và viết:

*Ten_bien_pointer

Ví dụ, giả sử bộ nhớ có trạng thái như trong hình trên:

char c = 0xAA;
char* cp = &c;

trong trường hợp này, *cp có thể được sử dụng thay cho c, và bằng câu lệnh:

*cp = 0x01;

ta có thể gán giá trị 0x01 cho c.
Như vậy, việc tham chiếu gián tiếp (indirection / 間接参照) đến một biến trong không gian bộ nhớ thông qua biến kiểu con trỏ (pointer type / ポインタ型変数) được gọi là gián tiếp tham chiếu (indirection / 間接参照).

Lưu ý rằng, nếu khai báo một biến kiểu con trỏ mà không kèm theo khởi tạo, thì biến con trỏ đó sẽ chứa một giá trị không xác định (indeterminate value / 不定な値). Do không biết giá trị địa chỉ không xác định này đang trỏ tới đâu trong bộ nhớ, nên nếu ghi giá trị vào vị trí được tham chiếu gián tiếp bởi một biến con trỏ như vậy, chương trình có thể hoạt động sai (undefined behavior / 未定義動作).

Vì vậy, nên khai báo biến kiểu con trỏ kèm theo khởi tạo, theo dạng:

Kiểu_dữ_liệu_được_trỏ *Tên_biến_pointer = &Tên_biến;

tức là khai báo đồng thời với việc khởi tạo địa chỉ mà biến con trỏ sẽ trỏ tới.

Con trỏ đa dụng và con trỏ rỗng(汎用ポインタと空ポインタ)

Thông thường, nếu T1 và T2 là hai kiểu khác nhau, thì không thể gán một giá trị con trỏ kiểu T2* cho biến con trỏ kiểu T1*. Tuy nhiên, trong C có một kiểu con trỏ đặc biệt gọi là con trỏ đa dụng (generic pointer / 汎用ポインタ), đó là:

void*

Biến thuộc kiểu con trỏ đa dụng void* này có thể được gán bất kỳ con trỏ của kiểu nào, và ngược lại, giá trị của con trỏ đa dụng (void*) cũng có thể được gán cho bất kỳ biến con trỏ kiểu nào khác.

				
					int i = 0;
void *a = &i;
int *ip = a;				
			

(1) Kiểu con trỏ đa dụng void*

※ Đối với con trỏ kiểu void*, có thể khởi tạo (initialize) và gán (assign) bằng con trỏ của bất kỳ kiểu nào.
※ Ngược lại, bất kỳ con trỏ của kiểu nào cũng có thể được khởi tạo hoặc gán từ một con trỏ kiểu void*.

(2) Con trỏ rỗng

Hằng số số nguyên 0 hoặc hằng số NULL

※ Con trỏ rỗng (null pointer / 空ポインタ) là một hằng số đặc biệt của kiểu void*, biểu thị một giá trị con trỏ không trỏ tới đối tượng nào.
※ Con trỏ rỗng không nhất thiết có nghĩa là địa chỉ bộ nhớ 0.
※ Khi con trỏ rỗng được xử lý như giá trị số, nó được xem là hằng số số nguyên 0.

Ngoài ra, như một giá trị con trỏ đặc biệt, người ta chuẩn bị con trỏ rỗng (null pointer / 空ポインタ). Con trỏ rỗng được biểu diễn bằng:

  • hằng số số nguyên 0, hoặc

  • hằng số NULL

và kiểu của nó là void*, tức là kiểu con trỏ đa dụng (generic pointer / 汎用ポインタ型). Vì vậy, khi khai báo một biến con trỏ mà chưa thể khởi tạo bằng một địa chỉ thích hợp, thì nên khởi tạo nó bằng con trỏ rỗng, ví dụ:

int *ip = NULL;

Bằng cách này, ta biểu thị rõ ràng rằng con trỏ đó hiện vẫn chưa trỏ tới đâu cả. Làm như vậy cho phép, tại những chỗ xử lý con trỏ, kiểm tra xem giá trị con trỏ có phải là con trỏ rỗng hay không, từ đó ngăn ngừa lỗi gián tiếp tham chiếu (indirect reference / 間接参照) từ một con trỏ chưa được thiết lập giá trị.

Tham chiếu gián tiếp đến cấu trúc (indirect reference / 間接参照)

Sau đây, chúng ta sẽ giải thích cách tham chiếu gián tiếp (indirect reference / 間接参照) đối với cấu trúc (structure / 構造体) và mảng (array / 配列).

Việc tham chiếu gián tiếp toàn bộ một biến cấu trúc thì không khác gì so với trường hợp tham chiếu gián tiếp một biến thông thường. Tuy nhiên, khi tham chiếu tới các thành viên (member / メンバ) của cấu trúc, thay vì viết:

(*con_trỏ_tới_cấu_trúc).tên_thành_viên

ta có thể sử dụng toán tử -> (arrow operator) và viết dưới dạng rút gọn: 

con_trỏ_tới_cấu_trúc->tên_thành_viên

Dạng rút gọn này giúp mã nguồn ngắn gọn, dễ đọc hơn, và là cách viết được sử dụng phổ biến khi truy cập thành viên của cấu trúc thông qua con trỏ.

(1) Tham chiếu gián tiếp tới cấu trúc (structure / 構造体)

*Ten_bien_con_tro_cau_truc

(2) Tham chiếu gián tiếp tới thành viên của cấu trúc (structure member / 構造体メンバ)

Ten_bien_con_tro_cau_truc->Ten_thanh_vien

※ Biểu thức trên tương đương với:

(* Ten_bien_con_tro_cau_truc).Ten_thanh_vien

Ví dụ: Gián tiếp tham chiếu phần ảo (imaginary part / 虚数部) của số phức cn và gán giá trị 2.0

struct ComplexNumber cn = { 3.0, 4.0 };
struct ComplexNumber *cnp = &cn;
cnp->image = 2.0;

Trong ví dụ này, cnp là con trỏ (pointer / ポインタ) trỏ tới cấu trúc cn, và toán tử -> được dùng để truy cập trực tiếp thành viên image của cấu trúc thông qua con trỏ.

Tham chiếu gián tiếp tới mảng(配列への間接参照 / indirect reference to array)

Tham chiếu gián tiếp tới mảng là một trong những cơ chế thú vị trong nội bộ của ngôn ngữ C. Thực ra, tên mảng (array name / 配列名) biểu diễn giá trị con trỏ (pointer value / ポインタ値) trỏ tới phần tử đầu tiên (first element / 先頭要素) của mảng.

Vì vậy,

*Ten_mang

là phép tham chiếu gián tiếp tới phần tử đầu tiên của mảng. Và toán tử [], vốn dùng để tham chiếu phần tử, không chỉ dùng cho mảng mà còn có thể dùng cho biến con trỏ (pointer variable / ポインタ型変数) dưới dạng:

Ten_bien_con_tro[chi_so]

Hãy xem ví dụ trong hình bên dưới. Một mảng kiểu doublea và một con trỏ kiểu double*ap được khai báo, trong đó ap được khởi tạo bằng a. Khi đó,

ap[1]

chính là phép tham chiếu gián tiếp tới phần tử thứ 1 của cùng mảng a, tức là a[1].

Điểm cần đặc biệt chú ý ở đây là: cùng phần tử thứ 1, a[1] cũng có thể được tham chiếu bằng biểu thức:

*(ap + 1)

Nếu ta coi biến con trỏ chỉ đơn thuần là một giá trị số nguyên lưu địa chỉ bộ nhớ (memory address / メモリアドレス), thì (ap + 1) dường như biểu diễn địa chỉ (x + 1). Tuy nhiên, như hình bên dưới cho thấy, phần tử thứ 1, a[1] không nằm tại địa chỉ x + 1, mà nằm tại địa chỉ:

x + sizeof(double)

Do đó, nếu phép cộng con trỏ chỉ đơn thuần là cộng số nguyên, thì biểu thức:

*(ap + 1)

sẽ không thể tham chiếu gián tiếp đúng tới phần tử a[1].

Điều này là do phép toán con trỏ (pointer arithmetic / ポインタ演算) có một cơ chế đặc biệt. Giả sử có một biến con trỏ tới kiểu T:

 
T* tp;

Khi j là một số nguyên, thì phép toán:

tp + j

thực chất thực hiện phép tính địa chỉ:

Dia_chi_tp_dang_giu + (sizeof(T) * j)

Nói cách khác, việc cộng hoặc trừ một số nguyên với con trỏ chính là đang tính toán giá trị con trỏ trỏ tới phần tử cách vị trí ban đầu j phần tử về phía trước hoặc phía sau.

Vì lý do đó, hai cách viết sau:

pointer[j] và,
*(pointer + j)

là hoàn toàn tương đương về mặt chức năng.

Thực tế, đối với hai toán hạng o1o2, biểu thức dùng toán tử []:

o1[o2]

được định nghĩa tương đương với:

*(o1 + o2)

(1) Quan hệ giữa tên mảng và địa chỉ

&Ten_mang là cú pháp lỗi. Tên mảng tự nó biểu diễn địa chỉ của phần tử đầu tiên trong mảng. Ví dụ:

double a[3];
double *ap = a;
*a; *ap;  /* cả hai đều tham chiếu gián tiếp (indirect reference / 間接参照) tới a[0] */

(2) Tham chiếu gián tiếp tới phần tử thứ j của mảng

Ten_bien_con_tro[j] ,Tương đương với:
*(Ten_bien_con_tro + j)

Ví dụ (trong trường hợp hình minh họa bên dưới):

double a[3] = {4.0, 7.5, 1.0};

double *ap = a;
ap[0]; *ap; /* cả hai đều tham chiếu gián tiếp tới a[0] */
ap[1];  /* tham chiếu gián tiếp tới a[1] */
*(ap + 1);  /* tham chiếu gián tiếp tới a[1] */

Lưu ý về tên mảng (配列名の注意点)

Như đã thấy ở trên, thoạt nhìn có thể nghĩ rằng tên mảng có thể được sử dụng như một biến con trỏ (pointer variable), nhưng thực tế không phải như vậy.

Ví dụ:

int ia[5];
int* iap = ia;

Trong trường hợp này, đối với tên mảng ia, không thể gán giá trị con trỏ như sau:

ia = iap; /* không hợp lệ */

Theo nghĩa đó, có thể xem tên mảng như một hằng con trỏ (pointer constant). Tuy nhiên, tên mảng không chỉ đơn thuần là một hằng con trỏ. Ví dụ, tên mảng còn giữ thông tin về kích thước của chính mảng đó. Với khai báo:

int ia[5];
int* iap = ia;

thì:

sizeof ia, sẽ trả về:sizeof(int) * 5,
trong khi đó:
sizeof iap, chỉ trả về:sizeof(int*)

tức là kích thước của biến con trỏ, chứ không phải kích thước của mảng.

※Tóm tắt:

  • Tên mảng có thể được xem như một hằng con trỏ (pointer constant), nhưng nó là thứ nhiều hơn một hằng con trỏ đơn thuần.

  • Bằng cách sử dụng toán tử truy cập phần tử [], có thể xử lý vùng nhớ mà con trỏ trỏ tới như một mảng.

  • Toán tử [] và toán tử * chỉ khác nhau về cách viết (syntax), còn bản chất hoạt động (semantics) thì giống nhau.

Định nghĩa tên thay thế cho kiểu bằng  typedef 

Như ví dụ minh họa bên dưới, bằng cách sử dụng từ khóa dành riêng typedef, ta có thể định nghĩa tên thay thế (alias) T2 cho một kiểu T1 nhất định.

Cách định nghĩa là: sau từ khóa typedef, ta viết dạng khai báo biến của kiểu T1.
Tuy nhiên, tại vị trí vốn là tên biến, ta thay vào đó tên kiểu mới T2 mà ta muốn định nghĩa.

Về bản chất, các kiểu dẫn xuất (derived type) thường được biểu diễn bằng nhiều từ hoặc ký hiệu để tạo thành một kiểu duy nhất.
Bằng cách sử dụng typedef, ta có thể định nghĩa một tên kiểu mới chỉ gồm một từ, đại diện cho những kiểu phức tạp đó. (Chú ý typedef là từ khoá ngôn ngữ C/C++, được Compiler (trình biên dịch) xử lý, khác với #define là từ khoá của bộ tiền xử lý).

Trong ví dụ:

  • Định nghĩa tên thay thế cho:

    • enum Country

    • struct ComplexNumber

    • union CharOrDouble
      lần lượt là:

    • Country

    • ComplexNumber

    • CharOrDouble

  • Ngoài ra:

    • Định nghĩa ArrayOfInt5 là tên thay thế cho kiểu mảng gồm 5 phần tử int

    • Định nghĩa PointerToInt là tên thay thế cho kiểu con trỏ tới int

				
					typedef  <dạng_khai_báo_biến>
// Tại vị trí tên biến, hãy ghi “tên kiểu mới” mà bạn muốn định nghĩa.

// Ví dụ
typedef int Integer; // Định nghĩa "Integer" là tên thay thế cho kiểu "int"
typedef enum Country {
    America, Japan, UK, USA = 0
} Country;  // Định nghĩa "Country" là tên thay thế cho kiểu "enum Country"
typedef struct ComplexNumber {
    double real; double image;
} Complex Number; // Định nghĩa "ComplexNumber" là tên thay thế cho kiểu "struct ComplexNumber"
typedef union CharOrDouble {
    char c; double d;
} CharOrDouble; // Định nghĩa "CharOrDouble" là tên thay thế cho kiểu "union CharOrDouble"
typedef int ArrayOfInt5 [5]; // Định nghĩa "ArrayOfInt5" là tên thay thế cho mảng 5 phần tử kiểu int
typedef int * PointerToInt; // Định nghĩa "PointerToInt" là tên thay thế cho con trỏ tới int				
			

Leave a Comment

Chia sẻ:

0.1. Những “thông tin” sử dụng trong ngôn ngữ C

Or copy link

CONTENTS