Bài 20.226 phút đọc
Khai báo và định nghĩa
Sau bài này bạn sẽ làm được
- Phân biệt khai báo với định nghĩa cho biến, hàm và kiểu
- Phát biểu quy tắc một định nghĩa và hậu quả khi vi phạm
- Dùng khai báo trước để chỉ cần biết kiểu tồn tại
- Đọc và hiểu lỗi multiple definition của trình liên kết
Hai từ này nghe như đồng nghĩa trong tiếng Việt lẫn tiếng Anh, và trong C chúng là hai chuyện hoàn toàn khác nhau. Không phân biệt được chúng thì mọi lỗi của trình liên kết đều là bí ẩn.
#Hai khái niệm, một cú pháp
Khai báo
Nói với trình biên dịch rằng một cái tên tồn tại và nó có kiểu gì. Không cấp phát gì, không sinh mã gì. Lặp lại bao nhiêu lần cũng được.
Định nghĩa
Tạo ra thứ đó thật sự: cấp chỗ trong bộ nhớ cho biến, sinh mã máy cho hàm, quy định bố cục cho kiểu. Chỉ được đúng một lần trong cả chương trình.
| Dòng mã | Là gì | Ghi chú |
|---|---|---|
| extern int g_dem; | Khai báo | Có extern, không có giá trị đầu |
| int g_dem; | Định nghĩa tạm thời | Trường hợp đặc biệt, xem Bài 20.3 |
| int g_dem = 0; | Định nghĩa | Có giá trị đầu |
| int cong(int, int); | Khai báo | Chỉ có nguyên mẫu |
| int cong(int a, int b) { return a + b; } | Định nghĩa | Có thân hàm |
| struct S; | Khai báo | Khai báo trước, chưa biết bên trong |
| struct S { int x; }; | Định nghĩa | Đã biết bố cục |
| typedef struct S S; | Khai báo tên kiểu | Không định nghĩa struct S |
/* Khai bao: lap lai duoc bao nhieu lan cung khong sao */
extern int g_dem;
extern int g_dem;
extern int g_dem;
int cong(int, int);
int cong(int, int);
/* Dinh nghia: chi mot lan */
int g_dem = 0;
int cong(int a, int b) { return a + b; }terminal
gcc -std=c11 -Wall -Wextra -fsyntax-only lap-lai.c
(khong co dong nao)
#Quy tắc một định nghĩa
Quy tắc một định nghĩa
Mỗi hàm và mỗi biến có liên kết ngoài chỉ được định nghĩa đúng một lần trong toàn bộ chương trình, kể cả mọi thư viện được liên kết vào.
Ví dụ vi phạm
/* cau-hinh.h */
#ifndef CAU_HINH_H
#define CAU_HINH_H
int g_muc_log = 1; /* DINH NGHIA trong header: SAI */
#endif
/* x1.c */
#include "cau-hinh.h"
int f1(void) { return g_muc_log; }
/* x2.c */
#include "cau-hinh.h"
int main(void) { return f1() + g_muc_log; }terminal
gcc -std=c11 -o xa x1.c x2.c
x2.o:x2.c:(.data+0x0): multiple definition of `g_muc_log' x1.o:x1.c:(.data+0x0): first defined here collect2: error: ld returned 1 exit status
Định nghĩa trong header
/* cau-hinh.h */
int g_muc_log = 1;Khai báo trong .h, định nghĩa trong .c
/* cau-hinh.h */
extern int g_muc_log; /* KHAI BAO, lap lai duoc */
/* cau-hinh.c */
#include "cau-hinh.h"
int g_muc_log = 1; /* DINH NGHIA, dung mot lan */terminal
gcc -std=c11 -fno-common -o yc y1.c y2.c && ./yc; echo "exit=$?"
exit=0
#Khai báo trước
struct SinhVien; /* khai bao truoc */
int db_luu(const struct SinhVien *sv); /* dung duoc: chi can con tro */
void db_in(struct SinhVien sv); /* LOI: can biet kich thuoc */
struct SinhVien sv; /* LOI, gcc bao:
error: storage size of 'sv'
isn't known */Khai báo trước
Nói rằng một kiểu tồn tại mà không nói bên trong nó có gì. Đủ để khai báo con trỏ tới nó, không đủ để tạo biến hay truy cập trường.
| Việc muốn làm | Chỉ khai báo trước có đủ không | Vì sao |
|---|---|---|
| Khai báo con trỏ: struct S *p; | Đủ | Con trỏ luôn cùng kích thước |
| Tham số hàm là con trỏ | Đủ | Như trên |
| Giá trị trả về là con trỏ | Đủ | Như trên |
| Tạo biến: struct S s; | Không đủ | Cần biết cấp bao nhiêu byte |
| Truy cập trường: p->x | Không đủ | Cần biết x ở độ lệch nào |
| Dùng sizeof(struct S) | Không đủ | Cần biết kích thước |
| Truyền theo giá trị | Không đủ | Cần biết kích thước để sao chép |
Ứng dụng: bớt include trong header
/* Cach 1: include */
/* luutru.h */
#include "sinhvien.h" /* keo theo ca sinhvien.h */
MaLoi lt_ghi(const char *ten, const SinhVien *ds, size_t n);
/* Cach 2: khai bao truoc */
/* luutru.h */
typedef struct SinhVien SinhVien; /* khong keo theo gi */
MaLoi lt_ghi(const char *ten, const SinhVien *ds, size_t n);
/* luutru.c thi VAN phai include sinhvien.h, vi cai dat
can truy cap truong. Nhung moi tep .c khac chi include
luutru.h thi khong phai phan tich sinhvien.h nua. */#Đọc lỗi của trình liên kết
| Thông báo | Nghĩa là | Cách sửa |
|---|---|---|
| undefined reference to `f' | Có khai báo nhưng không tìm thấy định nghĩa ở đâu | Thêm tệp .c chứa f vào lệnh liên kết, hoặc viết thân hàm |
| multiple definition of `g' | Tìm thấy hơn một định nghĩa | Chuyển định nghĩa ra khỏi header, hoặc thêm static |
| conflicting types for `f' | Hai khai báo cùng tên khác kiểu | Đây là lỗi biên dịch, không phải liên kết. Sửa nguyên mẫu |
| undefined reference to `main' | Không có hàm main trong số các tệp liên kết | Quên thêm main.c vào lệnh |
terminal
# Quên thêm sinhvien.c vào lệnh liên kết
gcc -Iinclude src/main.c src/quanly.c -o app
/usr/bin/ld: quanly.o: in function `ql_them': quanly.c:(.text+0x2a): undefined reference to `sv_kiem_tat_ca' collect2: error: ld returned 1 exit status
# Thêm vào là hết
gcc -Iinclude src/main.c src/quanly.c src/sinhvien.c -o app
(khong co dong nao)
Bảng phân biệt lỗi biên dịch với lỗi liên kết
| Dấu hiệu | Giai đoạn | Nhìn vào đâu |
|---|---|---|
| Thông báo có tên tệp và số dòng | Biên dịch | Đúng dòng đó trong mã nguồn |
| Thông báo nhắc tới tệp .o | Liên kết | Danh sách tệp trong lệnh gcc |
| Thông báo bắt đầu bằng ld hoặc collect2 | Liên kết | Như trên |
| Có chữ error: kèm mã nguồn được trích ra | Biên dịch | Cú pháp và kiểu |
| undefined reference, multiple definition | Liên kết | Ký hiệu, xem Bài 20.3 |
Tự làm thử
- Viết một tệp có ba dòng
extern int g;giống hệt nhau và xác nhận nó biên dịch sạch. - Đặt
int g = 1;trong header, include từ hai tệp.c, rồi đọc thông báo lỗi và chỉ ra nó tới từ giai đoạn nào. - Sửa lại bằng
externvà xác nhận chương trình chạy. - Viết
struct S;rồi thử khai báostruct S s;vàstruct S *p;, xem cái nào lỗi. - Cố ý quên một tệp
.ctrong lệnh liên kết và tập đọc thông báoundefined reference. - Viết chương trình dùng
sqrtrồi thử cảgcc -lm a.cvàgcc a.c -lm.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Khai báo nói một cái tên tồn tại và lặp lại được. Định nghĩa tạo ra nó thật sự và chỉ được một lần.
- Chính vì khai báo lặp lại được mà một header include được từ hai mươi tệp
.c. - Định nghĩa kiểu, hàm
staticvà macro là ba thứ được phép lặp lại, nên chúng đặt được trong header. - Khai báo trước đủ cho con trỏ, không đủ cho biến, cho truy cập trường, hay cho
sizeof. - Thông báo nhắc tệp
.olà lỗi liên kết, nên hãy nhìn vào lệnh build chứ không phải mã nguồn.